From fb744a24fa060759074d9374c3e02c20bb42e1e5 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:06:19 +0200 Subject: [PATCH 001/188] Clarify how the dataloader works with HF dataset (#3910) --- datasets/doc/source/tutorial-quickstart.ipynb | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/datasets/doc/source/tutorial-quickstart.ipynb b/datasets/doc/source/tutorial-quickstart.ipynb index d0f37ed311dd..ca6700aa31a3 100644 --- a/datasets/doc/source/tutorial-quickstart.ipynb +++ b/datasets/doc/source/tutorial-quickstart.ipynb @@ -439,6 +439,36 @@ "dataloader = DataLoader(partition_torch, batch_size=64)" ] }, + { + "cell_type": "markdown", + "id": "b93678a5", + "metadata": {}, + "source": "The `Dataloader` created this way does not return a `Tuple` when iterating over it but a `Dict` with the names of the columns as keys and features as values. Look below for an example." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5edd3ce2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Return type when iterating over dataloader: \n", + "torch.Size([64, 3, 32, 32])\n", + "torch.Size([64])\n" + ] + } + ], + "source": [ + "for batch in dataloader:\n", + " print(f\"Return type when iterating over a dataloader: {type(batch)}\")\n", + " print(batch[\"img\"].shape)\n", + " print(batch[\"label\"].shape)\n", + " break" + ] + }, { "cell_type": "markdown", "id": "71531613", From d648c78d9c71c46050124f2f9b18b1f77158c8a5 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:15:30 +0200 Subject: [PATCH 002/188] docs(datasets) Add info about distinguishing features of Flower Datasets (#3903) --- datasets/doc/source/index.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/datasets/doc/source/index.rst b/datasets/doc/source/index.rst index 3302dd758dd6..db4c8c3f20bf 100644 --- a/datasets/doc/source/index.rst +++ b/datasets/doc/source/index.rst @@ -111,6 +111,29 @@ How To Use the library ---------------------- Learn how to use the ``flwr-datasets`` library from the :doc:`tutorial-quickstart` examples . +Distinguishing Features +----------------------- +What makes Flower Datasets stand out from other libraries? + +* Access to the largest online repository of datasets: + + * The library functionality is independent of the dataset, so you can use any dataset available on [πŸ€—Hugging Face Datasets](https://huggingface.co/datasets), which means that others can immediately benefit from the dataset you added. + + * Out-of-the-box reproducibility across different projects. + + * Access to naturally dividable datasets (with some notion of id) and datasets typically used in centralized ML that need partitioning. + +* Customizable levels of dataset heterogeneity: + + * Each ``Partitioner`` takes arguments that allow you to customize the partitioning scheme to your needs. + + * Partitioning can also be applied to the dataset with naturally available division. + +* Flexible and open for extensions API. + + * New custom partitioning schemes (``Partitioner`` subclasses) integrated with the whole ecosystem. + + Join the Flower Community ------------------------- From b1ea24d0bc9c40ad4350fb7f026244de0cb30956 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 25 Jul 2024 17:55:31 +0200 Subject: [PATCH 003/188] ci(datasets) Add dev scripts to Flower Datasets (#3914) --- datasets/dev/build-flwr-datasets-docs.sh | 16 +++++++++++++++ datasets/dev/format.sh | 16 +++++++++++++++ datasets/dev/publish.sh | 21 ++++++++++++++++++++ datasets/dev/rm-caches.sh | 25 ++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100755 datasets/dev/publish.sh create mode 100755 datasets/dev/rm-caches.sh diff --git a/datasets/dev/build-flwr-datasets-docs.sh b/datasets/dev/build-flwr-datasets-docs.sh index aefa47f147f8..ed41a87a414b 100755 --- a/datasets/dev/build-flwr-datasets-docs.sh +++ b/datasets/dev/build-flwr-datasets-docs.sh @@ -1,4 +1,20 @@ #!/bin/bash + +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + # Generating the docs, rename and move the files such that the meet the convention used in Flower. # Note that it involves two runs of sphinx-build that are necessary. # The first run generates the .rst files (and the html files that are discarded) diff --git a/datasets/dev/format.sh b/datasets/dev/format.sh index c6977982dd6c..b7dca9accabf 100755 --- a/datasets/dev/format.sh +++ b/datasets/dev/format.sh @@ -1,4 +1,20 @@ #!/bin/bash + +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + set -e cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ diff --git a/datasets/dev/publish.sh b/datasets/dev/publish.sh new file mode 100755 index 000000000000..d76ce6de7879 --- /dev/null +++ b/datasets/dev/publish.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +set -e +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ + +python -m poetry publish -u __token__ -p ${PYPI_TOKEN} diff --git a/datasets/dev/rm-caches.sh b/datasets/dev/rm-caches.sh new file mode 100755 index 000000000000..8de3aa940d8e --- /dev/null +++ b/datasets/dev/rm-caches.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +set -e +cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ + +find . -type d -name __pycache__ -exec rm -r {} \+ +rm -rf .mypy_cache +rm -rf .pytest_cache +rm -rf .cache +rm -rf doc/build From af6d8ddd4d704cac457b4d3f43079a3e6277329e Mon Sep 17 00:00:00 2001 From: Meng Yan Date: Fri, 26 Jul 2024 15:21:48 +0800 Subject: [PATCH 004/188] Correct the document (#3920) --- doc/source/tutorial-quickstart-pytorch.rst | 2 +- doc/source/tutorial-quickstart-scikitlearn.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial-quickstart-pytorch.rst b/doc/source/tutorial-quickstart-pytorch.rst index 895590808a2b..6eb1f283c35e 100644 --- a/doc/source/tutorial-quickstart-pytorch.rst +++ b/doc/source/tutorial-quickstart-pytorch.rst @@ -159,7 +159,7 @@ Implementing :code:`NumPyClient` usually means defining the following methods #. :code:`fit` * set the local model weights * train the local model - * receive the updated local model weights + * return the updated local model weights #. :code:`evaluate` * test the local model diff --git a/doc/source/tutorial-quickstart-scikitlearn.rst b/doc/source/tutorial-quickstart-scikitlearn.rst index 93322842cc70..fc3b58925c06 100644 --- a/doc/source/tutorial-quickstart-scikitlearn.rst +++ b/doc/source/tutorial-quickstart-scikitlearn.rst @@ -123,7 +123,7 @@ Implementing :code:`NumPyClient` usually means defining the following methods #. :code:`fit` * set the local model weights * train the local model - * receive the updated local model weights + * return the updated local model weights #. :code:`evaluate` * test the local model From 3479703a520838dd29b0fc303c55485bf71a47c1 Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 26 Jul 2024 10:32:08 +0100 Subject: [PATCH 005/188] docs(framework) Add quickstart tutorial for MLX (#3915) Co-authored-by: Charles Beauville Co-authored-by: Daniel J. Beutel Co-authored-by: Chong Shen Ng --- doc/source/index.rst | 3 +- doc/source/tutorial-quickstart-mlx.rst | 375 +++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 doc/source/tutorial-quickstart-mlx.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index a0115620fce9..399f27d49596 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -48,6 +48,7 @@ A learning-oriented series of federated learning tutorials, the best place to st tutorial-quickstart-pytorch tutorial-quickstart-tensorflow + tutorial-quickstart-mlx tutorial-quickstart-huggingface tutorial-quickstart-jax tutorial-quickstart-pandas @@ -58,7 +59,7 @@ A learning-oriented series of federated learning tutorials, the best place to st tutorial-quickstart-android tutorial-quickstart-ios -QUICKSTART TUTORIALS: :doc:`PyTorch ` | :doc:`TensorFlow ` | :doc:`πŸ€— Transformers ` | :doc:`JAX ` | :doc:`Pandas ` | :doc:`fastai ` | :doc:`PyTorch Lightning ` | :doc:`scikit-learn ` | :doc:`XGBoost ` | :doc:`Android ` | :doc:`iOS ` +QUICKSTART TUTORIALS: :doc:`PyTorch ` | :doc:`TensorFlow ` | :doc:`MLX ` | :doc:`πŸ€— Transformers ` | :doc:`JAX ` | :doc:`Pandas ` | :doc:`fastai ` | :doc:`PyTorch Lightning ` | :doc:`scikit-learn ` | :doc:`XGBoost ` | :doc:`Android ` | :doc:`iOS ` We also made video tutorials for PyTorch: diff --git a/doc/source/tutorial-quickstart-mlx.rst b/doc/source/tutorial-quickstart-mlx.rst new file mode 100644 index 000000000000..593603cd8ae2 --- /dev/null +++ b/doc/source/tutorial-quickstart-mlx.rst @@ -0,0 +1,375 @@ +.. _quickstart-mlx: + + +Quickstart MLX +============== + + +In this tutorial we will learn how to train simple MLP on MNIST using Flower and MLX. + +First of all, it is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. + +Let's use `flwr new` to create a complete Flower+MLX project. It will generate all the files needed to run, by default with the Simulation Engine, a federation of 10 nodes using `FedAvg `_. The dataset will be partitioned using Flower Dataset's `IidPartitioner `_. + +Now that we have a rough idea of what this example is about, let's get started. We first need to create an MLX project. You can do this by running the command below. You will be prompted to give a name to your project as well as typing your developer name.: + +.. code-block:: shell + + $ flwr new --framework MLX + +After running it you'll notice a new directory with your project name has been created. It should have the following structure: + +.. code-block:: shell + + β”œβ”€β”€ + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp + β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp + β”‚ └── task.py # Defines your model, training and data loading + β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs + └── README.md + + +If you haven't yet installed the project and its dependencies, you can do so by: + +.. code-block:: shell + + # From the directory where your pyproject.toml is + $ pip install -e . + +To run the project do: + +.. code-block:: shell + + # Run with default arguments + $ flwr run . + +With default argumnets you will see an output like this one: + +.. code-block:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Requesting initial parameters from one random client + WARNING : FAB ID is not provided; the default ClientApp will be loaded. + INFO : Received initial parameters from one random client + INFO : Evaluating initial global parameters + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + WARNING : No fit_metrics_aggregation_fn provided + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + WARNING : No evaluate_metrics_aggregation_fn provided + INFO : + INFO : [ROUND 2] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [ROUND 3] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 8.15s + INFO : History (loss, distributed): + INFO : round 1: 2.243802046775818 + INFO : round 2: 2.101812958717346 + INFO : round 3: 1.7419301986694335 + INFO : + + +You can also override the parameters defined in `[tool.flwr.app.config]` section in the `pyproject.toml` like this: + +.. code-block:: shell + + # Override some arguments + $ flwr run . --run-config num-server-rounds=5,lr=0.05 + + +What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the `ClientApp` and defining the `ServerApp`. + +The Data +-------- + +We will use `flwr_datasets` to easily download and partition the `MNIST` dataset. +In this example you'll make use of the `IidPartitioner `_ to generate `num_partitions` partitions. +You can choose `other partitioners `_ available in Flower Datasets: + +.. code-block:: python + + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + partition_splits = partition.train_test_split(test_size=0.2, seed=42) + + partition_splits["train"].set_format("numpy") + partition_splits["test"].set_format("numpy") + + train_partition = partition_splits["train"].map( + lambda img: { + "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 + }, + input_columns="image", + ) + test_partition = partition_splits["test"].map( + lambda img: { + "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 + }, + input_columns="image", + ) + + data = ( + train_partition["img"], + train_partition["label"].astype(np.uint32), + test_partition["img"], + test_partition["label"].astype(np.uint32), + ) + + train_images, train_labels, test_images, test_labels = map(mx.array, data) + + +The Model +--------- + +We define the model as in the centralized MLX example, it's a simple MLP: + +.. code-block:: python + + class MLP(nn.Module): + """A simple MLP.""" + + def __init__( + self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int + ): + super().__init__() + layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] + self.layers = [ + nn.Linear(idim, odim) + for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) + ] + + def __call__(self, x): + for l in self.layers[:-1]: + x = mx.maximum(l(x), 0.0) + return self.layers[-1](x) + +We also define some utility functions to test our model and to iterate over batches. + +.. code-block:: python + + def loss_fn(model, X, y): + return mx.mean(nn.losses.cross_entropy(model(X), y)) + + + def eval_fn(model, X, y): + return mx.mean(mx.argmax(model(X), axis=1) == y) + + + def batch_iterate(batch_size, X, y): + perm = mx.array(np.random.permutation(y.size)) + for s in range(0, y.size, batch_size): + ids = perm[s : s + batch_size] + yield X[ids], y[ids] + + +The ClientApp +------------- + +The main changes we have to make to use `MLX` with `Flower` will be found in +the `get_params` and `set_params` functions. Indeed, MLX doesn't +provide an easy way to convert the model parameters into a list of `np.array` objects +(the format we need for the serialization of the messages to work). + +The way MLX stores its parameters is as follows: + +.. code-block:: shell + + { + "layers": [ + {"weight": mlx.core.array, "bias": mlx.core.array}, + {"weight": mlx.core.array, "bias": mlx.core.array}, + ..., + {"weight": mlx.core.array, "bias": mlx.core.array} + ] + } + +Therefore, to get our list of `np.array`s, we need to extract each array and +convert them into a NumPy array: + +.. code-block:: python + + def get_params(model): + layers = model.parameters()["layers"] + return [np.array(val) for layer in layers for _, val in layer.items()] + + +For the `set_params` function, we perform the reverse operation. We receive +a list of NumPy arrays and want to convert them into MLX parameters. Therefore, we +iterate through pairs of parameters and assign them to the `weight` and `bias` +keys of each layer dict: + +.. code-block:: python + + def set_params(model, parameters): + new_params = {} + new_params["layers"] = [ + {"weight": mx.array(parameters[i]), "bias": mx.array(parameters[i + 1])} + for i in range(0, len(parameters), 2) + ] + model.update(new_params) + + +The rest of the functionality is directly inspired by the centralized case. The `fit()` +method in the client trains the model using the local dataset: + +.. code-block:: python + + def fit(self, parameters, config): + self.set_parameters(parameters) + for _ in range(self.num_epochs): + for X, y in batch_iterate( + self.batch_size, self.train_images, self.train_labels + ): + _, grads = self.loss_and_grad_fn(self.model, X, y) + self.optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), self.optimizer.state) + return self.get_parameters(config={}), len(self.train_images), {} + + +Here, after updating the parameters, we perform the training as in the +centralized case, and return the new parameters. + +And for the `evaluate` method of the client: + +.. code-block:: python + + def evaluate(self, parameters, config): + self.set_parameters(parameters) + accuracy = eval_fn(self.model, self.test_images, self.test_labels) + loss = loss_fn(self.model, self.test_images, self.test_labels) + return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} + + +We also begin by updating the parameters with the ones sent by the server, and +then we compute the loss and accuracy using the functions defined above. In the +constructor of the `FlowerClient` we instantiate the `MLP` model as well as other +components such as the optimizer. + +Putting everything together we have: + +.. code-block:: python + + class FlowerClient(NumPyClient): + def __init__( + self, + data, + num_layers, + hidden_dim, + num_classes, + batch_size, + learning_rate, + num_epochs, + ): + self.num_layers = num_layers + self.hidden_dim = hidden_dim + self.num_classes = num_classes + self.batch_size = batch_size + self.learning_rate = learning_rate + self.num_epochs = num_epochs + + self.train_images, self.train_labels, self.test_images, self.test_labels = data + self.model = MLP( + num_layers, self.train_images.shape[-1], hidden_dim, num_classes + ) + self.optimizer = optim.SGD(learning_rate=learning_rate) + self.loss_and_grad_fn = nn.value_and_grad(self.model, loss_fn) + self.num_epochs = num_epochs + self.batch_size = batch_size + + def get_parameters(self, config): + return get_params(self.model) + + def set_parameters(self, parameters): + set_params(self.model, parameters) + + def fit(self, parameters, config): + self.set_parameters(parameters) + for _ in range(self.num_epochs): + for X, y in batch_iterate( + self.batch_size, self.train_images, self.train_labels + ): + _, grads = self.loss_and_grad_fn(self.model, X, y) + self.optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), self.optimizer.state) + return self.get_parameters(config={}), len(self.train_images), {} + + def evaluate(self, parameters, config): + self.set_parameters(parameters) + accuracy = eval_fn(self.model, self.test_images, self.test_labels) + loss = loss_fn(self.model, self.test_images, self.test_labels) + return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} + + +Finally, we can construct a `ClientApp` using the `FlowerClient` defined above by means of a `client_fn` callback: + +.. code-block:: python + + def client_fn(context: Context): + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + data = load_data(partition_id, num_partitions) + + num_layers = context.run_config["num-layers"] + hidden_dim = context.run_config["hidden-dim"] + num_classes = 10 + batch_size = context.run_config["batch-size"] + learning_rate = context.run_config["lr"] + num_epochs = context.run_config["local-epochs"] + + # Return Client instance + return FlowerClient( + data, num_layers, hidden_dim, num_classes, batch_size, learning_rate, num_epochs + ).to_client() + + + # Flower ClientApp + app = ClientApp(client_fn) + +The ServerApp +------------- + +To construct a `ServerApp` we define a `server_fn()` callback with an identical signature +to that of `client_fn()` but the return type is `ServerAppComponents `_ as opposed to a `Client `_. In this example we use the `FedAvg` strategy. + +.. code-block:: python + + def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] + + # Define strategy + strategy = FedAvg() + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + + # Create ServerApp + app = ServerApp(server_fn=server_fn) + + +Congratulations! +You've successfully built and run your first federated learning system. +The `source code `_ of the extended version of this tutorial can be found in :code:`examples/quickstart-mlx`. From 1f25189192433f6f13bf26fd1b7adaa8d214bdb9 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 26 Jul 2024 11:50:54 +0200 Subject: [PATCH 006/188] docs(framework:skip) Rename stable version variable (#3917) --- doc/source/conf.py | 2 +- ...contributor-how-to-build-docker-images.rst | 8 ++--- doc/source/how-to-install-flower.rst | 2 +- doc/source/how-to-run-flower-using-docker.rst | 34 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 489e141eecac..998c50b1dd7b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -92,7 +92,7 @@ release = "1.11.0" # The current released version rst_prolog = """ -.. |current_flwr_version| replace:: 1.10.0 +.. |stable_flwr_version| replace:: 1.10.0 """ # -- General configuration --------------------------------------------------- diff --git a/doc/source/contributor-how-to-build-docker-images.rst b/doc/source/contributor-how-to-build-docker-images.rst index c5b2295a0f03..457193c93db6 100644 --- a/doc/source/contributor-how-to-build-docker-images.rst +++ b/doc/source/contributor-how-to-build-docker-images.rst @@ -65,7 +65,7 @@ Building the base image * - ``FLWR_VERSION`` - Version of Flower to be installed. - Yes - - :substitution-code:`|current_flwr_version|` + - :substitution-code:`|stable_flwr_version|` * - ``FLWR_PACKAGE`` - The Flower package to be installed. - No @@ -73,7 +73,7 @@ Building the base image The following example creates a base Ubuntu/Alpine image with Python 3.11.0, pip 23.0.1, -setuptools 69.0.2 and Flower |current_flwr_version|: +setuptools 69.0.2 and Flower |stable_flwr_version|: .. code-block:: bash :substitutions: @@ -81,7 +81,7 @@ setuptools 69.0.2 and Flower |current_flwr_version|: $ cd src/docker/base/ $ docker build \ --build-arg PYTHON_VERSION=3.11.0 \ - --build-arg FLWR_VERSION=|current_flwr_version| \ + --build-arg FLWR_VERSION=|stable_flwr_version| \ --build-arg PIP_VERSION=23.0.1 \ --build-arg SETUPTOOLS_VERSION=69.0.2 \ -t flwr_base:0.1.0 . @@ -107,7 +107,7 @@ Building the SuperLink/SuperNode or ServerApp image * - ``BASE_IMAGE`` - The Tag of the Flower base image. - Yes - - :substitution-code:`|current_flwr_version|-py3.10-ubuntu22.04` + - :substitution-code:`|stable_flwr_version|-py3.10-ubuntu22.04` The following example creates a SuperLink/SuperNode or ServerApp image with the official Flower base image: diff --git a/doc/source/how-to-install-flower.rst b/doc/source/how-to-install-flower.rst index 3fdb5cbd3959..10f0f221b4c8 100644 --- a/doc/source/how-to-install-flower.rst +++ b/doc/source/how-to-install-flower.rst @@ -51,7 +51,7 @@ The following command can be used to verify if Flower was successfully installed :substitutions: python -c "import flwr;print(flwr.__version__)" - |current_flwr_version| + |stable_flwr_version| Advanced installation options diff --git a/doc/source/how-to-run-flower-using-docker.rst b/doc/source/how-to-run-flower-using-docker.rst index 08d590d16a45..6272c5c131c3 100644 --- a/doc/source/how-to-run-flower-using-docker.rst +++ b/doc/source/how-to-run-flower-using-docker.rst @@ -39,10 +39,10 @@ If you're looking to try out Flower, you can use the following command: .. code-block:: bash :substitutions: - $ docker run --rm -p 9091:9091 -p 9092:9092 flwr/superlink:|current_flwr_version| --insecure + $ docker run --rm -p 9091:9091 -p 9092:9092 flwr/superlink:|stable_flwr_version| --insecure -The command pulls the Docker image with the tag :substitution-code:`|current_flwr_version|` from Docker Hub. The tag specifies -the Flower version. In this case, Flower |current_flwr_version|. The ``--rm`` flag tells Docker to remove the +The command pulls the Docker image with the tag :substitution-code:`|stable_flwr_version|` from Docker Hub. The tag specifies +the Flower version. In this case, Flower |stable_flwr_version|. The ``--rm`` flag tells Docker to remove the container after it exits. .. note:: @@ -68,7 +68,7 @@ You can use ``--help`` to view all available flags that the SuperLink supports: .. code-block:: bash :substitutions: - $ docker run --rm flwr/superlink:|current_flwr_version| --help + $ docker run --rm flwr/superlink:|stable_flwr_version| --help Mounting a volume to store the state on the host system ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -91,7 +91,7 @@ container. Furthermore, we use the flag ``--database`` to specify the name of th $ mkdir state $ sudo chown -R 49999:49999 state $ docker run --rm \ - -p 9091:9091 -p 9092:9092 --volume ./state/:/app/state flwr/superlink:|current_flwr_version| \ + -p 9091:9091 -p 9092:9092 --volume ./state/:/app/state flwr/superlink:|stable_flwr_version| \ --insecure \ --database state.db @@ -122,7 +122,7 @@ with the ``--ssl-ca-certfile``, ``--ssl-certfile`` and ``--ssl-keyfile`` flag. $ docker run --rm \ -p 9091:9091 -p 9092:9092 \ - --volume ./certificates/:/app/certificates/:ro flwr/superlink:|current_flwr_version| \ + --volume ./certificates/:/app/certificates/:ro flwr/superlink:|stable_flwr_version| \ --ssl-ca-certfile certificates/ca.crt \ --ssl-certfile certificates/server.pem \ --ssl-keyfile certificates/server.key @@ -201,7 +201,7 @@ The ``Dockerfile.supernode`` contains the instructions that assemble the SuperNo .. code-block:: dockerfile :substitutions: - FROM flwr/supernode:|current_flwr_version| + FROM flwr/supernode:|stable_flwr_version| WORKDIR /app @@ -211,7 +211,7 @@ The ``Dockerfile.supernode`` contains the instructions that assemble the SuperNo COPY client.py ./ ENTRYPOINT ["flower-client-app", "client:app"] -In the first two lines, we instruct Docker to use the SuperNode image tagged :substitution-code:`|current_flwr_version|` as a base +In the first two lines, we instruct Docker to use the SuperNode image tagged :substitution-code:`|stable_flwr_version|` as a base image and set our working directory to ``/app``. The following instructions will now be executed in the ``/app`` directory. Next, we install the ClientApp dependencies by copying the ``requirements.txt`` file into the image and run ``pip install``. In the last two lines, @@ -273,7 +273,7 @@ To see all available flags that the SuperNode supports, run: .. code-block:: bash :substitutions: - $ docker run --rm flwr/supernode:|current_flwr_version| --help + $ docker run --rm flwr/supernode:|stable_flwr_version| --help Enabling SSL for secure connections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -324,14 +324,14 @@ The ``Dockerfile.serverapp`` contains the instructions that assemble the ServerA .. code-block:: dockerfile :substitutions: - FROM flwr/serverapp:|current_flwr_version| + FROM flwr/serverapp:|stable_flwr_version| WORKDIR /app COPY server.py ./ ENTRYPOINT ["flower-server-app", "server:app"] -In the first two lines, we instruct Docker to use the ServerApp image tagged :substitution-code:`|current_flwr_version|` as a base +In the first two lines, we instruct Docker to use the ServerApp image tagged :substitution-code:`|stable_flwr_version|` as a base image and set our working directory to ``/app``. The following instructions will now be executed in the ``/app`` directory. In the last two lines, we copy the ``server.py`` module into the image and set the entry point to ``flower-server-app`` with the argument ``server:app``. @@ -391,7 +391,7 @@ To see all available flags that the ServerApp supports, run: .. code-block:: bash :substitutions: - $ docker run --rm flwr/serverapp:|current_flwr_version| --help + $ docker run --rm flwr/serverapp:|stable_flwr_version| --help Enabling SSL for secure connections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -426,7 +426,7 @@ Run the Docker image with the ``-u`` flag and specify ``root`` as the username: .. code-block:: bash :substitutions: - $ docker run --rm -u root flwr/superlink:|current_flwr_version| + $ docker run --rm -u root flwr/superlink:|stable_flwr_version| This command will run the Docker container with root user privileges. @@ -438,7 +438,7 @@ missing system dependencies, you can use the ``USER root`` directive within your .. code-block:: dockerfile :substitutions: - FROM flwr/supernode:|current_flwr_version| + FROM flwr/supernode:|stable_flwr_version| # Switch to root user USER root @@ -473,12 +473,12 @@ updates of system dependencies that should not change the functionality of Flowe want to ensure that you always use the same image, you can specify the hash of the image instead of the tag. -The following command returns the current image hash referenced by the :substitution-code:`superlink:|current_flwr_version|` tag: +The following command returns the current image hash referenced by the :substitution-code:`superlink:|stable_flwr_version|` tag: .. code-block:: bash :substitutions: - $ docker inspect --format='{{index .RepoDigests 0}}' flwr/superlink:|current_flwr_version| + $ docker inspect --format='{{index .RepoDigests 0}}' flwr/superlink:|stable_flwr_version| flwr/superlink@sha256:985c24b2b337ab7f15a554fde9d860cede95079bcaa244fda8f12c0805e34c7d Next, we can pin the hash when running a new SuperLink container: @@ -498,4 +498,4 @@ To set a variable inside a Docker container, you can use the ``-e = :substitutions: $ docker run -e FLWR_TELEMETRY_ENABLED=0 \ - --rm flwr/superlink:|current_flwr_version| --insecure + --rm flwr/superlink:|stable_flwr_version| --insecure From 11702caa04a98f72dcbeaeb5864dca08e3e872ec Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 26 Jul 2024 11:06:23 +0100 Subject: [PATCH 007/188] feat(framework:skip) Add verbose logging for `SimulationEngine` (#3913) --- src/py/flwr/superexec/simulation.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/superexec/simulation.py b/src/py/flwr/superexec/simulation.py index d4cc489e24ab..a2048f179938 100644 --- a/src/py/flwr/superexec/simulation.py +++ b/src/py/flwr/superexec/simulation.py @@ -63,8 +63,10 @@ class SimulationEngine(Executor): def __init__( self, num_supernodes: Optional[int] = None, + verbose: Optional[bool] = False, ) -> None: self.num_supernodes = num_supernodes + self.verbose = verbose @override def set_config( @@ -80,6 +82,8 @@ def set_config( Supported configuration key/value pairs: - "num-supernodes": int Number of nodes to register for the simulation. + - "verbose": bool + Set verbosity of logs. """ if num_supernodes := config.get("num-supernodes"): if not isinstance(num_supernodes, int): @@ -97,6 +101,13 @@ def set_config( "positive integer." ) + if verbose := config.get("verbose"): + if not isinstance(verbose, bool): + raise ValueError( + "The `verbose` value must be a string `true` or `false`." + ) + self.verbose = verbose + @override def start_run( self, @@ -121,10 +132,11 @@ def start_run( fab_path = install_from_fab(fab_file, None, True) # Install FAB Python package - subprocess.check_call( + subprocess.run( [sys.executable, "-m", "pip", "install", "--no-deps", str(fab_path)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stdout=None if self.verbose else subprocess.DEVNULL, + stderr=None if self.verbose else subprocess.DEVNULL, + check=True, ) # Load and validate config From fa894663702c8c0bb46ec79226e0a8e6071e3b87 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:21:05 +0200 Subject: [PATCH 008/188] Fix formatting (#3923) --- datasets/doc/source/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datasets/doc/source/index.rst b/datasets/doc/source/index.rst index db4c8c3f20bf..84e25a920f2f 100644 --- a/datasets/doc/source/index.rst +++ b/datasets/doc/source/index.rst @@ -1,7 +1,7 @@ Flower Datasets =============== -Flower Datasets (``flower-datasets ``) is a library that enables the quick and easy creation of datasets for federated learning/analytics/evaluation. It enables heterogeneity (non-iidness) simulation and division of datasets with the preexisting notion of IDs. The library was created by the ``Flower Labs`` team that also created `Flower `_ : A Friendly Federated Learning Framework. +Flower Datasets (``flwr-datasets``) is a library that enables the quick and easy creation of datasets for federated learning/analytics/evaluation. It enables heterogeneity (non-iidness) simulation and division of datasets with the preexisting notion of IDs. The library was created by the ``Flower Labs`` team that also created `Flower `_ : A Friendly Federated Learning Framework. Flower Datasets Framework ------------------------- @@ -117,7 +117,7 @@ What makes Flower Datasets stand out from other libraries? * Access to the largest online repository of datasets: - * The library functionality is independent of the dataset, so you can use any dataset available on [πŸ€—Hugging Face Datasets](https://huggingface.co/datasets), which means that others can immediately benefit from the dataset you added. + * The library functionality is independent of the dataset, so you can use any dataset available on `πŸ€—Hugging Face Datasets `_, which means that others can immediately benefit from the dataset you added. * Out-of-the-box reproducibility across different projects. From 606bab4f28d2162168f635738714ae10e5b5924d Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 26 Jul 2024 16:03:03 +0100 Subject: [PATCH 009/188] docs(framework) Update Flower tutorial series to Flower Next (part 1 of 4) (#3295) --- ...ries-get-started-with-flower-pytorch.ipynb | 311 +++++++++++------- 1 file changed, 193 insertions(+), 118 deletions(-) diff --git a/doc/source/tutorial-series-get-started-with-flower-pytorch.ipynb b/doc/source/tutorial-series-get-started-with-flower-pytorch.ipynb index d8e6e58fafab..4d126d67463c 100644 --- a/doc/source/tutorial-series-get-started-with-flower-pytorch.ipynb +++ b/doc/source/tutorial-series-get-started-with-flower-pytorch.ipynb @@ -9,11 +9,13 @@ "\n", "Welcome to the Flower federated learning tutorial!\n", "\n", - "In this notebook, we'll build a federated learning system using Flower, [Flower Datasets](https://flower.ai/docs/datasets/) and PyTorch. In part 1, we use PyTorch for the model training pipeline and data loading. In part 2, we continue to federate the PyTorch-based pipeline using Flower.\n", + "In this notebook, we'll build a federated learning system using the Flower framework, Flower Datasets and PyTorch. In part 1, we use PyTorch for the model training pipeline and data loading. In part 2, we federate the PyTorch project using Flower.\n", "\n", - "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Slack to connect, ask questions, and get help: [Join Slack](https://flower.ai/join-slack) 🌼 We'd love to hear from you in the `#introductions` channel! And if anything is unclear, head over to the `#questions` channel.\n", + "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Flower Discuss and the Flower Slack to connect, ask questions, and get help:\n", + "> - [Join Flower Discuss](https://discuss.flower.ai/) We'd love to hear from you in the `Introduction` topic! If anything is unclear, post in `Flower Help - Beginners`.\n", + "> - [Join Flower Slack](https://flower.ai/join-slack) We'd love to hear from you in the `#introductions` channel! If anything is unclear, head over to the `#questions` channel.\n", "\n", - "Let's get started!" + "Let's get started! 🌼" ] }, { @@ -29,7 +31,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Installing dependencies\n", + "### Install dependencies\n", "\n", "Next, we install the necessary packages for PyTorch (`torch` and `torchvision`), Flower Datasets (`flwr-datasets`) and Flower (`flwr`):" ] @@ -40,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install -q flwr[simulation] flwr_datasets[vision] torch torchvision matplotlib" + "!pip install -q flwr[simulation] flwr-datasets[vision] torch torchvision matplotlib" ] }, { @@ -68,14 +70,17 @@ "from datasets.utils.logging import disable_progress_bar\n", "from torch.utils.data import DataLoader\n", "\n", - "import flwr as fl\n", - "from flwr.common import Metrics\n", + "import flwr\n", + "from flwr.client import Client, ClientApp, NumPyClient\n", + "from flwr.common import Metrics, Context\n", + "from flwr.server import ServerApp, ServerConfig, ServerAppComponents\n", + "from flwr.server.strategy import FedAvg\n", + "from flwr.simulation import run_simulation\n", "from flwr_datasets import FederatedDataset\n", "\n", "DEVICE = torch.device(\"cpu\") # Try \"cuda\" to train on GPU\n", - "print(\n", - " f\"Training on {DEVICE} using PyTorch {torch.__version__} and Flower {fl.__version__}\"\n", - ")\n", + "print(f\"Training on {DEVICE}\")\n", + "print(f\"Flower {flwr.__version__} / PyTorch {torch.__version__}\")\n", "disable_progress_bar()" ] }, @@ -90,8 +95,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\n", - "### Loading the data\n", + "### Load the data\n", "\n", "Federated learning can be applied to many different types of tasks across different domains. In this tutorial, we introduce federated learning by training a simple convolutional neural network (CNN) on the popular CIFAR-10 dataset. CIFAR-10 can be used to train image classifiers that distinguish between images from ten different classes: 'airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', and 'truck'." ] @@ -100,17 +104,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We simulate having multiple datasets from multiple organizations (also called the \"cross-silo\" setting in federated learning) by splitting the original CIFAR-10 dataset into multiple partitions. Each partition will represent the data from a single organization. We're doing this purely for experimentation purposes, in the real world there's no need for data splitting because each organization already has their own data (so the data is naturally partitioned).\n", + "We simulate having multiple datasets from multiple organizations (also called the \"cross-silo\" setting in federated learning) by splitting the original CIFAR-10 dataset into multiple partitions. Each partition will represent the data from a single organization. We're doing this purely for experimentation purposes, in the real world there's no need for data splitting because each organization already has their own data (the data is naturally partitioned).\n", "\n", - "Each organization will act as a client in the federated learning system. So having ten organizations participate in a federation means having ten clients connected to the federated learning server.\n" + "Each organization will act as a client in the federated learning system. Having ten organizations participate in a federation means having ten clients connected to the federated learning server.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "\n", - "Let's now create the Federated Dataset abstraction that from `flwr-datasets` that partitions the CIFAR-10. We will create small training and test set for each edge device and wrap each of them into a PyTorch `DataLoader`:" + "We use the Flower Datasets library (`flwr-datasets`) to partition CIFAR-10 into ten partitions using `FederatedDataset`. We will create a small training and test set for each of the ten organizations and wrap each of these into a PyTorch `DataLoader`:" ] }, { @@ -123,46 +126,40 @@ "BATCH_SIZE = 32\n", "\n", "\n", - "def load_datasets():\n", + "def load_datasets(partition_id: int):\n", " fds = FederatedDataset(dataset=\"cifar10\", partitioners={\"train\": NUM_CLIENTS})\n", + " partition = fds.load_partition(partition_id)\n", + " # Divide data on each node: 80% train, 20% test\n", + " partition_train_test = partition.train_test_split(test_size=0.2, seed=42)\n", + " pytorch_transforms = transforms.Compose(\n", + " [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]\n", + " )\n", "\n", " def apply_transforms(batch):\n", " # Instead of passing transforms to CIFAR10(..., transform=transform)\n", " # we will use this function to dataset.with_transform(apply_transforms)\n", " # The transforms object is exactly the same\n", - " transform = transforms.Compose(\n", - " [\n", - " transforms.ToTensor(),\n", - " transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),\n", - " ]\n", - " )\n", - " batch[\"img\"] = [transform(img) for img in batch[\"img\"]]\n", + " batch[\"img\"] = [pytorch_transforms(img) for img in batch[\"img\"]]\n", " return batch\n", "\n", " # Create train/val for each partition and wrap it into DataLoader\n", - " trainloaders = []\n", - " valloaders = []\n", - " for partition_id in range(NUM_CLIENTS):\n", - " partition = fds.load_partition(partition_id, \"train\")\n", - " partition = partition.with_transform(apply_transforms)\n", - " partition = partition.train_test_split(train_size=0.8, seed=42)\n", - " trainloaders.append(DataLoader(partition[\"train\"], batch_size=BATCH_SIZE))\n", - " valloaders.append(DataLoader(partition[\"test\"], batch_size=BATCH_SIZE))\n", + " partition_train_test = partition_train_test.with_transform(apply_transforms)\n", + " trainloader = DataLoader(\n", + " partition_train_test[\"train\"], batch_size=BATCH_SIZE, shuffle=True\n", + " )\n", + " valloader = DataLoader(partition_train_test[\"test\"], batch_size=BATCH_SIZE)\n", " testset = fds.load_split(\"test\").with_transform(apply_transforms)\n", " testloader = DataLoader(testset, batch_size=BATCH_SIZE)\n", - " return trainloaders, valloaders, testloader\n", - "\n", - "\n", - "trainloaders, valloaders, testloader = load_datasets()" + " return trainloader, valloader, testloader" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now have a list of ten training sets and ten validation sets (`trainloaders` and `valloaders`) representing the data of ten different organizations. Each `trainloader`/`valloader` pair contains 4000 training examples and 1000 validation examples. There's also a single `testloader` (we did not split the test set). Again, this is only necessary for building research or educational systems, actual federated learning systems have their data naturally distributed across multiple partitions.\n", + "We now have a function that can return a training set and validation set (`trainloader` and `valloader`) representing one dataset from one of ten different organizations. Each `trainloader`/`valloader` pair contains 4000 training examples and 1000 validation examples. There's also a single `testloader` (we did not split the test set). Again, this is only necessary for building research or educational systems, actual federated learning systems have their data naturally distributed across multiple partitions.\n", "\n", - "Let's take a look at the first batch of images and labels in the first training set (i.e., `trainloaders[0]`) before we move on:" + "Let's take a look at the first batch of images and labels in the first training set (i.e., `trainloader` from `partition_id=0`) before we move on:" ] }, { @@ -171,11 +168,14 @@ "metadata": {}, "outputs": [], "source": [ - "batch = next(iter(trainloaders[0]))\n", + "trainloader, _, _ = load_datasets(partition_id=0)\n", + "batch = next(iter(trainloader))\n", "images, labels = batch[\"img\"], batch[\"label\"]\n", + "\n", "# Reshape and convert images to a NumPy array\n", "# matplotlib requires images with the shape (height, width, 3)\n", "images = images.permute(0, 2, 3, 1).numpy()\n", + "\n", "# Denormalize\n", "images = images / 2 + 0.5\n", "\n", @@ -185,7 +185,7 @@ "# Loop over the images and plot them\n", "for i, ax in enumerate(axs.flat):\n", " ax.imshow(images[i])\n", - " ax.set_title(trainloaders[0].dataset.features[\"label\"].int2str([labels[i]])[0])\n", + " ax.set_title(trainloader.dataset.features[\"label\"].int2str([labels[i]])[0])\n", " ax.axis(\"off\")\n", "\n", "# Show the plot\n", @@ -197,7 +197,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The output above shows a random batch of images from the first `trainloader` in our list of ten `trainloaders`. It also prints the labels associated with each image (i.e., one of the ten possible labels we've seen above). If you run the cell again, you should see another batch of images." + "The output above shows a random batch of images from the `trainloader` from the first of ten partitions. It also prints the labels associated with each image (i.e., one of the ten possible labels we've seen above). If you run the cell again, you should see another batch of images." ] }, { @@ -219,7 +219,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Defining the model\n", + "### Define the model\n", "\n", "We use the simple CNN described in the [PyTorch tutorial](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#define-a-convolutional-neural-network):" ] @@ -309,9 +309,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Training the model\n", + "### Train the model\n", "\n", - "We now have all the basic building blocks we need: a dataset, a model, a training function, and a test function. Let's put them together to train the model on the dataset of one of our organizations (`trainloaders[0]`). This simulates the reality of most machine learning projects today: each organization has their own data and trains models only on this internal data: " + "We now have all the basic building blocks we need: a dataset, a model, a training function, and a test function. Let's put them together to train the model on the dataset of one of our organizations (`partition_id=0`). This simulates the reality of most machine learning projects today: each organization has their own data and trains models only on this internal data: " ] }, { @@ -320,8 +320,7 @@ "metadata": {}, "outputs": [], "source": [ - "trainloader = trainloaders[0]\n", - "valloader = valloaders[0]\n", + "trainloader, valloader, testloader = load_datasets(partition_id=0)\n", "net = Net().to(DEVICE)\n", "\n", "for epoch in range(5):\n", @@ -337,7 +336,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Training the simple CNN on our CIFAR-10 split for 5 epochs should result in a test set accuracy of about 41%, which is not good, but at the same time, it doesn't really matter for the purposes of this tutorial. The intent was just to show a simplistic centralized training pipeline that sets the stage for what comes next - federated learning!" + "Training the simple CNN on our CIFAR-10 split for 5 epochs should result in a test set accuracy of about 41%, which is not good, but at the same time, it doesn't really matter for the purposes of this tutorial. The intent was just to show a simple centralized training pipeline that sets the stage for what comes next - federated learning!" ] }, { @@ -353,13 +352,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Updating model parameters\n", + "### Update model parameters\n", "\n", - "In federated learning, the server sends the global model parameters to the client, and the client updates the local model with the parameters received from the server. It then trains the model on the local data (which changes the model parameters locally) and sends the updated/changed model parameters back to the server (or, alternatively, it sends just the gradients back to the server, not the full model parameters).\n", + "In federated learning, the server sends global model parameters to the client, and the client updates the local model with parameters received from the server. It then trains the model on the local data (which changes the model parameters locally) and sends the updated/changed model parameters back to the server (or, alternatively, it sends just the gradients back to the server, not the full model parameters).\n", "\n", "We need two helper functions to update the local model with parameters received from the server and to get the updated model parameters from the local model: `set_parameters` and `get_parameters`. The following two functions do just that for the PyTorch model above.\n", "\n", - "The details of how this works are not really important here (feel free to consult the PyTorch documentation if you want to learn more). In essence, we use `state_dict` to access PyTorch model parameter tensors. The parameter tensors are then converted to/from a list of NumPy ndarray's (which Flower knows how to serialize/deserialize):" + "The details of how this works are not really important here (feel free to consult the PyTorch documentation if you want to learn more). In essence, we use `state_dict` to access PyTorch model parameter tensors. The parameter tensors are then converted to/from a list of NumPy ndarray's (which the Flower `NumPyClient` knows how to serialize/deserialize):" ] }, { @@ -382,15 +381,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Implementing a Flower client\n", + "### Define the Flower ClientApp\n", "\n", - "With that out of the way, let's move on to the interesting part. Federated learning systems consist of a server and multiple clients. In Flower, we create clients by implementing subclasses of `flwr.client.Client` or `flwr.client.NumPyClient`. We use `NumPyClient` in this tutorial because it is easier to implement and requires us to write less boilerplate.\n", + "With that out of the way, let's move on to the interesting part. Federated learning systems consist of a server and multiple clients. In Flower, we create a `ServerApp` and a `ClientApp` to run the server-side and client-side code, respectively.\n", "\n", - "To implement the Flower client, we create a subclass of `flwr.client.NumPyClient` and implement the three methods `get_parameters`, `fit`, and `evaluate`:\n", + "The first step toward creating a `ClientApp` is to implement a subclasses of `flwr.client.Client` or `flwr.client.NumPyClient`. We use `NumPyClient` in this tutorial because it is easier to implement and requires us to write less boilerplate. To implement `NumPyClient`, we create a subclass that implements the three methods `get_parameters`, `fit`, and `evaluate`:\n", "\n", "* `get_parameters`: Return the current local model parameters\n", - "* `fit`: Receive model parameters from the server, train the model parameters on the local data, and return the (updated) model parameters to the server\n", - "* `evaluate`: Receive model parameters from the server, evaluate the model parameters on the local data, and return the evaluation result to the server\n", + "* `fit`: Receive model parameters from the server, train the model on the local data, and return the updated model parameters to the server\n", + "* `evaluate`: Receive model parameters from the server, evaluate the model on the local data, and return the evaluation result to the server\n", "\n", "We mentioned that our clients will use the previously defined PyTorch components for model training and evaluation. Let's see a simple Flower client implementation that brings everything together:" ] @@ -401,7 +400,7 @@ "metadata": {}, "outputs": [], "source": [ - "class FlowerClient(fl.client.NumPyClient):\n", + "class FlowerClient(NumPyClient):\n", " def __init__(self, net, trainloader, valloader):\n", " self.net = net\n", " self.trainloader = trainloader\n", @@ -425,13 +424,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Our class `FlowerClient` defines how local training/evaluation will be performed and allows Flower to call the local training/evaluation through `fit` and `evaluate`. Each instance of `FlowerClient` represents a *single client* in our federated learning system. Federated learning systems have multiple clients (otherwise, there's not much to federate), so each client will be represented by its own instance of `FlowerClient`. If we have, for example, three clients in our workload, then we'd have three instances of `FlowerClient`. Flower calls `FlowerClient.fit` on the respective instance when the server selects a particular client for training (and `FlowerClient.evaluate` for evaluation).\n", + "Our class `FlowerClient` defines how local training/evaluation will be performed and allows Flower to call the local training/evaluation through `fit` and `evaluate`. Each instance of `FlowerClient` represents a *single client* in our federated learning system. Federated learning systems have multiple clients (otherwise, there's not much to federate), so each client will be represented by its own instance of `FlowerClient`. If we have, for example, three clients in our workload, then we'd have three instances of `FlowerClient` (one on each of the machines we'd start the client on). Flower calls `FlowerClient.fit` on the respective instance when the server selects a particular client for training (and `FlowerClient.evaluate` for evaluation).\n", "\n", - "### Using the Virtual Client Engine\n", + "In this notebook, we want to simulate a federated learning system with 10 clients *on a single machine*. This means that the server and all 10 clients will live on a single machine and share resources such as CPU, GPU, and memory. Having 10 clients would mean having 10 instances of `FlowerClient` in memory. Doing this on a single machine can quickly exhaust the available memory resources, even if only a subset of these clients participates in a single round of federated learning.\n", "\n", - "In this notebook, we want to simulate a federated learning system with 10 clients on a single machine. This means that the server and all 10 clients will live on a single machine and share resources such as CPU, GPU, and memory. Having 10 clients would mean having 10 instances of `FlowerClient` in memory. Doing this on a single machine can quickly exhaust the available memory resources, even if only a subset of these clients participates in a single round of federated learning.\n", + "In addition to the regular capabilities where server and clients run on multiple machines, Flower, therefore, provides special simulation capabilities that create `FlowerClient` instances only when they are actually necessary for training or evaluation. To enable the Flower framework to create clients when necessary, we need to implement a function that creates a `FlowerClient` instance on demand. We typically call this function `client_fn`. Flower calls `client_fn` whenever it needs an instance of one particular client to call `fit` or `evaluate` (those instances are usually discarded after use, so they should not keep any local state). In federated learning experiments using Flower, clients are identified by a partition ID, or `partition-id`. This `partition-id` is used to load different local data partitions for different clients, as can be seen below. The value of `partition-id` is retrieved from the `node_config` dictionary in the `Context` object, which holds the information that persists throughout each training round. \n", "\n", - "In addition to the regular capabilities where server and clients run on multiple machines, Flower, therefore, provides special simulation capabilities that create `FlowerClient` instances only when they are actually necessary for training or evaluation. To enable the Flower framework to create clients when necessary, we need to implement a function called `client_fn` that creates a `FlowerClient` instance on demand. Flower calls `client_fn` whenever it needs an instance of one particular client to call `fit` or `evaluate` (those instances are usually discarded after use, so they should not keep any local state). Clients are identified by a client ID, or short `cid`. The `cid` can be used, for example, to load different local data partitions for different clients, as can be seen below:" + "With this, we have the class `FlowerClient` which defines client-side training/evaluation and `client_fn` which allows Flower to create `FlowerClient` instances whenever it needs to call `fit` or `evaluate` on one particular client. Last, but definitely not least, we create an instance of `ClientApp` and pass it the `client_fn`. `ClientApp` is the entrypoint that a running Flower client uses to call your code (as defined in, for example, `FlowerClient.fit`)." ] }, { @@ -440,7 +439,7 @@ "metadata": {}, "outputs": [], "source": [ - "def client_fn(cid: str) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " \"\"\"Create a Flower client representing a single organization.\"\"\"\n", "\n", " # Load model\n", @@ -448,25 +447,28 @@ "\n", " # Load data (CIFAR-10)\n", " # Note: each client gets a different trainloader/valloader, so each client\n", - " # will train and evaluate on their own unique data\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", + " # will train and evaluate on their own unique data partition\n", + " # Read the node_config to fetch data partition associated to this node\n", + " partition_id = context.node_config[\"partition-id\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id=partition_id)\n", + "\n", + " # Create a single Flower client representing a single organization\n", + " # FlowerClient is a subclass of NumPyClient, so we need to call .to_client()\n", + " # to convert it to a subclass of `flwr.client.Client`\n", + " return FlowerClient(net, trainloader, valloader).to_client()\n", + "\n", "\n", - " # Create a single Flower client representing a single organization\n", - " return FlowerClient(net, trainloader, valloader).to_client()" + "# Create the ClientApp\n", + "client = ClientApp(client_fn=client_fn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Starting the training\n", - "\n", - "We now have the class `FlowerClient` which defines client-side training/evaluation and `client_fn` which allows Flower to create `FlowerClient` instances whenever it needs to call `fit` or `evaluate` on one particular client. The last step is to start the actual simulation using `flwr.simulation.start_simulation`. \n", - "\n", - "The function `start_simulation` accepts a number of arguments, amongst them the `client_fn` used to create `FlowerClient` instances, the number of clients to simulate (`num_clients`), the number of federated learning rounds (`num_rounds`), and the strategy. The strategy encapsulates the federated learning approach/algorithm, for example, *Federated Averaging* (FedAvg).\n", + "### Define the Flower ServerApp\n", "\n", - "Flower has a number of built-in strategies, but we can also use our own strategy implementations to customize nearly all aspects of the federated learning approach. For this example, we use the built-in `FedAvg` implementation and customize it using a few basic parameters. The last step is the actual call to `start_simulation` which - you guessed it - starts the simulation:" + "On the server side, we need to configure a strategy which encapsulates the federated learning approach/algorithm, for example, *Federated Averaging* (FedAvg). Flower has a number of built-in strategies, but we can also use our own strategy implementations to customize nearly all aspects of the federated learning approach. For this example, we use the built-in `FedAvg` implementation and customize it using a few basic parameters:" ] }, { @@ -476,30 +478,94 @@ "outputs": [], "source": [ "# Create FedAvg strategy\n", - "strategy = fl.server.strategy.FedAvg(\n", + "strategy = FedAvg(\n", " fraction_fit=1.0, # Sample 100% of available clients for training\n", " fraction_evaluate=0.5, # Sample 50% of available clients for evaluation\n", " min_fit_clients=10, # Never sample less than 10 clients for training\n", " min_evaluate_clients=5, # Never sample less than 5 clients for evaluation\n", " min_available_clients=10, # Wait until all 10 clients are available\n", - ")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similar to `ClientApp`, we create a `ServerApp` using a utility function `server_fn`. In `server_fn`, we pass an instance of `ServerConfig` for defining the number of federated learning rounds (`num_rounds`) and we also pass the previously created `strategy`. The `server_fn` returns a `ServerAppComponents` object containing the settings that define the `ServerApp` behaviour. `ServerApp` is the entrypoint that Flower uses to call all your server-side code (for example, the strategy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def server_fn(context: Context) -> ServerAppComponents:\n", + " \"\"\"Construct components that set the ServerApp behaviour.\n", + "\n", + " You can use the settings in `context.run_config` to parameterize the\n", + " construction of all elements (e.g the strategy or the number of rounds)\n", + " wrapped in the returned ServerAppComponents object.\n", + " \"\"\"\n", + "\n", + " # Configure the server for 5 rounds of training\n", + " config = ServerConfig(num_rounds=5)\n", + "\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", "\n", - "# Specify the resources each of your clients need. By default, each\n", - "# client will be allocated 1x CPU and 0x GPUs\n", - "client_resources = {\"num_cpus\": 1, \"num_gpus\": 0.0}\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Run the training\n", + "\n", + "In simulation, we often want to control the amount of resources each client can use. In the next cell, we specify a `backend_config` dictionary with the `client_resources` key (required) for defining the amount of CPU and GPU resources each client can access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Specify the resources each of your clients need\n", + "# By default, each client will be allocated 1x CPU and 0x GPUs\n", + "backend_config = {\"client_resources\": {\"num_cpus\": 1, \"num_gpus\": 0.0}}\n", + "\n", + "# When running on GPU, assign an entire GPU for each client\n", "if DEVICE.type == \"cuda\":\n", - " # here we are assigning an entire GPU for each client.\n", - " client_resources = {\"num_cpus\": 1, \"num_gpus\": 1.0}\n", - " # Refer to our documentation for more details about Flower Simulations\n", - " # and how to setup these `client_resources`.\n", - "\n", - "# Start simulation\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=5),\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + " backend_config = {\"client_resources\": {\"num_cpus\": 1, \"num_gpus\": 1.0}}\n", + " # Refer to our Flower framework documentation for more details about Flower simulations\n", + " # and how to set up the `backend_config`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The last step is the actual call to `run_simulation` which - you guessed it - runs the simulation. `run_simulation` accepts a number of arguments:\n", + "- `server_app` and `client_app`: the previously created `ServerApp` and `ClientApp` objects, respectively\n", + "- `num_supernodes`: the number of `SuperNodes` to simulate which equals the number of clients for Flower simulation\n", + "- `backend_config`: the resource allocation used in this simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_CLIENTS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -511,9 +577,9 @@ "\n", "So how does this work? How does Flower execute this simulation?\n", "\n", - "When we call `start_simulation`, we tell Flower that there are 10 clients (`num_clients=10`). Flower then goes ahead an asks the `FedAvg` strategy to select clients. `FedAvg` knows that it should select 100% of the available clients (`fraction_fit=1.0`), so it goes ahead and selects 10 random clients (i.e., 100% of 10).\n", + "When we call `run_simulation`, we tell Flower that there are 10 clients (`num_supernodes=10`, where 1 `SuperNode` launches 1 `ClientApp`). Flower then goes ahead an asks the `ServerApp` to issue an instructions to those nodes using the `FedAvg` strategy. `FedAvg` knows that it should select 100% of the available clients (`fraction_fit=1.0`), so it goes ahead and selects 10 random clients (i.e., 100% of 10).\n", "\n", - "Flower then asks the selected 10 clients to train the model. When the server receives the model parameter updates from the clients, it hands those updates over to the strategy (*FedAvg*) for aggregation. The strategy aggregates those updates and returns the new global model, which then gets used in the next round of federated learning." + "Flower then asks the selected 10 clients to train the model. Each of the 10 `ClientApp` instances receives a message, which causes it to call `client_fn` to create an instance of `FlowerClient`. It then calls `.fit()` on each the `FlowerClient` instances and returns the resulting model parameter updates to the `ServerApp`. When the `ServerApp` receives the model parameter updates from the clients, it hands those updates over to the strategy (*FedAvg*) for aggregation. The strategy aggregates those updates and returns the new global model, which then gets used in the next round of federated learning." ] }, { @@ -546,36 +612,45 @@ " return {\"accuracy\": sum(accuracies) / sum(examples)}" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The only thing left to do is to tell the strategy to call this function whenever it receives evaluation metric dictionaries from the clients:" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Create FedAvg strategy\n", - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=1.0,\n", - " fraction_evaluate=0.5,\n", - " min_fit_clients=10,\n", - " min_evaluate_clients=5,\n", - " min_available_clients=10,\n", - " evaluate_metrics_aggregation_fn=weighted_average, # <-- pass the metric aggregation function\n", - ")\n", - "\n", - "# Start simulation\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=5),\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " \"\"\"Construct components that set the ServerApp behaviour.\n", + "\n", + " You can use settings in `context.run_config` to parameterize the\n", + " construction of all elements (e.g the strategy or the number of rounds)\n", + " wrapped in the returned ServerAppComponents object.\n", + " \"\"\"\n", + "\n", + " # Create FedAvg strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=1.0,\n", + " fraction_evaluate=0.5,\n", + " min_fit_clients=10,\n", + " min_evaluate_clients=5,\n", + " min_available_clients=10,\n", + " evaluate_metrics_aggregation_fn=weighted_average, # <-- pass the metric aggregation function\n", + " )\n", + "\n", + " # Configure the server for 5 rounds of training\n", + " config = ServerConfig(num_rounds=5)\n", + "\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create a new server instance with the updated FedAvg strategy\n", + "server = ServerApp(server_fn=server_fn)\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_CLIENTS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -605,7 +680,7 @@ "source": [ "## Next steps\n", "\n", - "Before you continue, make sure to join the Flower community on Slack: [Join Slack](https://flower.ai/join-slack/)\n", + "Before you continue, make sure to join the Flower community on Flower Discuss ([Join Flower Discuss](https://discuss.flower.ai)) and on Slack ([Join Slack](https://flower.ai/join-slack/)).\n", "\n", "There's a dedicated `#questions` channel if you need help, but we'd also love to hear who you are in `#introductions`!\n", "\n", @@ -620,11 +695,11 @@ "toc_visible": true }, "kernelspec": { - "display_name": "flwr", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } From a6abc1fa8395d881cdb5bba90abaff8be1662c4b Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 26 Jul 2024 16:41:45 +0100 Subject: [PATCH 010/188] docs(framework) Update Flower tutorial series to Flower Next (part 2 of 4) (#3313) --- ...-federated-learning-strategy-pytorch.ipynb | 453 +++++++++++------- 1 file changed, 289 insertions(+), 164 deletions(-) diff --git a/doc/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb b/doc/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb index e20a8d83f674..a361365f4fb0 100644 --- a/doc/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb +++ b/doc/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb @@ -9,11 +9,13 @@ "\n", "Welcome to the next part of the federated learning tutorial. In previous parts of this tutorial, we introduced federated learning with PyTorch and Flower ([part 1](https://flower.ai/docs/framework/tutorial-get-started-with-flower-pytorch.html)).\n", "\n", - "In this notebook, we'll begin to customize the federated learning system we built in the introductory notebook (again, using [Flower](https://flower.ai/) and [PyTorch](https://pytorch.org/)).\n", + "In this notebook, we'll begin to customize the federated learning system we built in the introductory notebook again, using the Flower framework, Flower Datasets, and PyTorch.\n", "\n", - "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Slack to connect, ask questions, and get help: [Join Slack](https://flower.ai/join-slack) 🌼 We'd love to hear from you in the `#introductions` channel! And if anything is unclear, head over to the `#questions` channel.\n", + "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Flower Discuss and the Flower Slack to connect, ask questions, and get help:\n", + "> - [Join Flower Discuss](https://discuss.flower.ai/) We'd love to hear from you in the `Introduction` topic! If anything is unclear, post in `Flower Help - Beginners`.\n", + "> - [Join Flower Slack](https://flower.ai/join-slack) We'd love to hear from you in the `#introductions` channel! If anything is unclear, head over to the `#questions` channel.\n", "\n", - "Let's move beyond FedAvg with Flower strategies!" + "Let's move beyond FedAvg with Flower strategies! 🌼" ] }, { @@ -40,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install -q flwr[simulation] torch torchvision" + "!pip install -q flwr[simulation] flwr-datasets[vision] torch torchvision" ] }, { @@ -64,15 +66,19 @@ "import torch.nn as nn\n", "import torch.nn.functional as F\n", "import torchvision.transforms as transforms\n", - "from torch.utils.data import DataLoader, random_split\n", - "from torchvision.datasets import CIFAR10\n", + "from torch.utils.data import DataLoader\n", "\n", - "import flwr as fl\n", + "import flwr\n", + "from flwr.client import Client, ClientApp, NumPyClient\n", + "from flwr.server import ServerApp, ServerConfig, ServerAppComponents\n", + "from flwr.server.strategy import FedAvg, FedAdagrad\n", + "from flwr.simulation import run_simulation\n", + "from flwr_datasets import FederatedDataset\n", + "from flwr.common import ndarrays_to_parameters, NDArrays, Scalar, Context\n", "\n", "DEVICE = torch.device(\"cpu\") # Try \"cuda\" to train on GPU\n", - "print(\n", - " f\"Training on {DEVICE} using PyTorch {torch.__version__} and Flower {fl.__version__}\"\n", - ")" + "print(f\"Training on {DEVICE}\")\n", + "print(f\"Flower {flwr.__version__} / PyTorch {torch.__version__}\")" ] }, { @@ -88,7 +94,7 @@ "source": [ "### Data loading\n", "\n", - "Let's now load the CIFAR-10 training and test set, partition them into ten smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`. We introduce a new parameter `num_clients` which allows us to call `load_datasets` with different numbers of clients." + "Let's now load the CIFAR-10 training and test set, partition them into ten smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`. We introduce a new parameter `num_partitions` which allows us to call `load_datasets` with different numbers of partitions." ] }, { @@ -97,37 +103,34 @@ "metadata": {}, "outputs": [], "source": [ - "NUM_CLIENTS = 10\n", + "NUM_PARTITIONS = 10\n", + "BATCH_SIZE = 32\n", "\n", "\n", - "def load_datasets(num_clients: int):\n", - " # Download and transform CIFAR-10 (train and test)\n", - " transform = transforms.Compose(\n", + "def load_datasets(partition_id: int, num_partitions: int):\n", + " fds = FederatedDataset(dataset=\"cifar10\", partitioners={\"train\": num_partitions})\n", + " partition = fds.load_partition(partition_id)\n", + " # Divide data on each node: 80% train, 20% test\n", + " partition_train_test = partition.train_test_split(test_size=0.2, seed=42)\n", + " pytorch_transforms = transforms.Compose(\n", " [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]\n", " )\n", - " trainset = CIFAR10(\"./dataset\", train=True, download=True, transform=transform)\n", - " testset = CIFAR10(\"./dataset\", train=False, download=True, transform=transform)\n", - "\n", - " # Split training set into `num_clients` partitions to simulate different local datasets\n", - " partition_size = len(trainset) // num_clients\n", - " lengths = [partition_size] * num_clients\n", - " datasets = random_split(trainset, lengths, torch.Generator().manual_seed(42))\n", - "\n", - " # Split each partition into train/val and create DataLoader\n", - " trainloaders = []\n", - " valloaders = []\n", - " for ds in datasets:\n", - " len_val = len(ds) // 10 # 10 % validation set\n", - " len_train = len(ds) - len_val\n", - " lengths = [len_train, len_val]\n", - " ds_train, ds_val = random_split(ds, lengths, torch.Generator().manual_seed(42))\n", - " trainloaders.append(DataLoader(ds_train, batch_size=32, shuffle=True))\n", - " valloaders.append(DataLoader(ds_val, batch_size=32))\n", - " testloader = DataLoader(testset, batch_size=32)\n", - " return trainloaders, valloaders, testloader\n", "\n", + " def apply_transforms(batch):\n", + " # Instead of passing transforms to CIFAR10(..., transform=transform)\n", + " # we will use this function to dataset.with_transform(apply_transforms)\n", + " # The transforms object is exactly the same\n", + " batch[\"img\"] = [pytorch_transforms(img) for img in batch[\"img\"]]\n", + " return batch\n", "\n", - "trainloaders, valloaders, testloader = load_datasets(NUM_CLIENTS)" + " partition_train_test = partition_train_test.with_transform(apply_transforms)\n", + " trainloader = DataLoader(\n", + " partition_train_test[\"train\"], batch_size=BATCH_SIZE, shuffle=True\n", + " )\n", + " valloader = DataLoader(partition_train_test[\"test\"], batch_size=BATCH_SIZE)\n", + " testset = fds.load_split(\"test\").with_transform(apply_transforms)\n", + " testloader = DataLoader(testset, batch_size=BATCH_SIZE)\n", + " return trainloader, valloader, testloader" ] }, { @@ -182,7 +185,8 @@ " net.train()\n", " for epoch in range(epochs):\n", " correct, total, epoch_loss = 0, 0, 0.0\n", - " for images, labels in trainloader:\n", + " for batch in trainloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " optimizer.zero_grad()\n", " outputs = net(images)\n", @@ -204,7 +208,8 @@ " correct, total, loss = 0, 0, 0.0\n", " net.eval()\n", " with torch.no_grad():\n", - " for images, labels in testloader:\n", + " for batch in testloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " outputs = net(images)\n", " loss += criterion(outputs, labels).item()\n", @@ -222,7 +227,7 @@ "source": [ "### Flower client\n", "\n", - "To implement the Flower client, we (again) create a subclass of `flwr.client.NumPyClient` and implement the three methods `get_parameters`, `fit`, and `evaluate`. Here, we also pass the `cid` to the client and use it log additional details:" + "To implement the Flower client, we (again) create a subclass of `flwr.client.NumPyClient` and implement the three methods `get_parameters`, `fit`, and `evaluate`. Here, we also pass the `partition_id` to the client and use it log additional details. We then create an instance of `ClientApp` and pass it the `client_fn`." ] }, { @@ -231,35 +236,43 @@ "metadata": {}, "outputs": [], "source": [ - "class FlowerClient(fl.client.NumPyClient):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerClient(NumPyClient):\n", + " def __init__(self, partition_id, net, trainloader, valloader):\n", + " self.partition_id = partition_id\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, config):\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.partition_id}] get_parameters\")\n", " return get_parameters(self.net)\n", "\n", " def fit(self, parameters, config):\n", - " print(f\"[Client {self.cid}] fit, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] fit, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " train(self.net, self.trainloader, epochs=1)\n", " return get_parameters(self.net), len(self.trainloader), {}\n", "\n", " def evaluate(self, parameters, config):\n", - " print(f\"[Client {self.cid}] evaluate, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] evaluate, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " loss, accuracy = test(self.net, self.valloader)\n", " return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}\n", "\n", "\n", - "def client_fn(cid) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerClient(cid, net, trainloader, valloader)" + "\n", + " # Read the node_config to fetch data partition associated to this node\n", + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + "\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerClient(partition_id, net, trainloader, valloader).to_client()\n", + "\n", + "\n", + "# Create the ClientApp\n", + "client = ClientApp(client_fn=client_fn)" ] }, { @@ -277,7 +290,7 @@ "source": [ "### Server-side parameter **initialization**\n", "\n", - "Flower, by default, initializes the global model by asking one random client for the initial parameters. In many cases, we want more control over parameter initialization though. Flower therefore allows you to directly pass the initial parameters to the Strategy:" + "Flower, by default, initializes the global model by asking one random client for the initial parameters. In many cases, we want more control over parameter initialization though. Flower therefore allows you to directly pass the initial parameters to the Strategy. We create an instance of `Net()` and get the paramaters as follows:" ] }, { @@ -287,30 +300,84 @@ "outputs": [], "source": [ "# Create an instance of the model and get the parameters\n", - "params = get_parameters(Net())\n", - "\n", - "# Pass parameters to the Strategy for server-side parameter initialization\n", - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=0.3,\n", - " fraction_evaluate=0.3,\n", - " min_fit_clients=3,\n", - " min_evaluate_clients=3,\n", - " min_available_clients=NUM_CLIENTS,\n", - " initial_parameters=fl.common.ndarrays_to_parameters(params),\n", - ")\n", - "\n", - "# Specify client resources if you need GPU (defaults to 1 CPU and 0 GPU)\n", - "client_resources = None\n", + "params = get_parameters(Net())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we create a `server_fn` that returns the components needed for the server. Within `server_fn`, we create a Strategy that uses the initial parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Create FedAvg strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=0.3,\n", + " fraction_evaluate=0.3,\n", + " min_fit_clients=3,\n", + " min_evaluate_clients=3,\n", + " min_available_clients=NUM_PARTITIONS,\n", + " initial_parameters=ndarrays_to_parameters(\n", + " params\n", + " ), # Pass initial model parameters\n", + " )\n", + "\n", + " # Configure the server for 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(strategy=strategy, config=config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Passing `initial_parameters` to the `FedAvg` strategy prevents Flower from asking one of the clients for the initial parameters. In `server_fn`, we pass this new `strategy` and a `ServerConfig` for defining the number of federated learning rounds (`num_rounds`). \n", + "\n", + "Similar to the `ClientApp`, we now create the `ServerApp` using the `server_fn`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create ServerApp\n", + "server = ServerApp(server_fn=server_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Last but not least, we specify the resources for each client and run the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Specify the resources each of your clients need\n", + "# If set to none, by default, each client will be allocated 2x CPU and 0x GPUs\n", + "backend_config = {\"client_resources\": None}\n", "if DEVICE.type == \"cuda\":\n", - " client_resources = {\"num_gpus\": 1}\n", - "\n", - "# Start simulation\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=3), # Just three rounds\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + " backend_config = {\"client_resources\": {\"num_gpus\": 1}}\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -318,7 +385,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Passing `initial_parameters` to the `FedAvg` strategy prevents Flower from asking one of the clients for the initial parameters. If we look closely, we can see that the logs do not show any calls to the `FlowerClient.get_parameters` method." + " If we look closely, we can see that the logs do not show any calls to the `FlowerClient.get_parameters` method." ] }, { @@ -327,7 +394,7 @@ "source": [ "### Starting with a customized strategy\n", "\n", - "We've seen the function `start_simulation` before. It accepts a number of arguments, amongst them the `client_fn` used to create `FlowerClient` instances, the number of clients to simulate `num_clients`, the number of rounds `num_rounds`, and the strategy.\n", + "We've seen the function `run_simulation` before. It accepts a number of arguments, amongst them the `server_app` which wraps around the strategy and number of training rounds, `client_app` which wraps around the `client_fn` used to create `FlowerClient` instances, and the number of clients to simulate which equals `num_supernodes`.\n", "\n", "The strategy encapsulates the federated learning approach/algorithm, for example, `FedAvg` or `FedAdagrad`. Let's try to use a different strategy this time:" ] @@ -338,23 +405,30 @@ "metadata": {}, "outputs": [], "source": [ - "# Create FedAdam strategy\n", - "strategy = fl.server.strategy.FedAdagrad(\n", - " fraction_fit=0.3,\n", - " fraction_evaluate=0.3,\n", - " min_fit_clients=3,\n", - " min_evaluate_clients=3,\n", - " min_available_clients=NUM_CLIENTS,\n", - " initial_parameters=fl.common.ndarrays_to_parameters(get_parameters(Net())),\n", - ")\n", - "\n", - "# Start simulation\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=3), # Just three rounds\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Create FedAdagrad strategy\n", + " strategy = FedAdagrad(\n", + " fraction_fit=0.3,\n", + " fraction_evaluate=0.3,\n", + " min_fit_clients=3,\n", + " min_evaluate_clients=3,\n", + " min_available_clients=NUM_PARTITIONS,\n", + " initial_parameters=ndarrays_to_parameters(params),\n", + " )\n", + " # Configure the server for 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -379,42 +453,72 @@ "metadata": {}, "outputs": [], "source": [ - "# The `evaluate` function will be by Flower called after every round\n", + "# The `evaluate` function will be called by Flower after every round\n", "def evaluate(\n", " server_round: int,\n", - " parameters: fl.common.NDArrays,\n", - " config: Dict[str, fl.common.Scalar],\n", - ") -> Optional[Tuple[float, Dict[str, fl.common.Scalar]]]:\n", + " parameters: NDArrays,\n", + " config: Dict[str, Scalar],\n", + ") -> Optional[Tuple[float, Dict[str, Scalar]]]:\n", " net = Net().to(DEVICE)\n", - " valloader = valloaders[0]\n", + " _, _, testloader = load_datasets(0, NUM_PARTITIONS)\n", " set_parameters(net, parameters) # Update model with the latest parameters\n", - " loss, accuracy = test(net, valloader)\n", + " loss, accuracy = test(net, testloader)\n", " print(f\"Server-side evaluation loss {loss} / accuracy {accuracy}\")\n", " return loss, {\"accuracy\": accuracy}" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We create a `FedAvg` strategy and pass `evaluate_fn` to it. Then, we create a `ServerApp` that uses this strategy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Create the FedAvg strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=0.3,\n", + " fraction_evaluate=0.3,\n", + " min_fit_clients=3,\n", + " min_evaluate_clients=3,\n", + " min_available_clients=NUM_PARTITIONS,\n", + " initial_parameters=ndarrays_to_parameters(params),\n", + " evaluate_fn=evaluate, # Pass the evaluation function\n", + " )\n", + " # Configure the server for 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we run the simulation." + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=0.3,\n", - " fraction_evaluate=0.3,\n", - " min_fit_clients=3,\n", - " min_evaluate_clients=3,\n", - " min_available_clients=NUM_CLIENTS,\n", - " initial_parameters=fl.common.ndarrays_to_parameters(get_parameters(Net())),\n", - " evaluate_fn=evaluate, # Pass the evaluation function\n", - ")\n", - "\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=3), # Just three rounds\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -433,15 +537,15 @@ "metadata": {}, "outputs": [], "source": [ - "class FlowerClient(fl.client.NumPyClient):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerClient(NumPyClient):\n", + " def __init__(self, pid, net, trainloader, valloader):\n", + " self.pid = pid # partition ID of a client\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, config):\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.pid}] get_parameters\")\n", " return get_parameters(self.net)\n", "\n", " def fit(self, parameters, config):\n", @@ -450,23 +554,28 @@ " local_epochs = config[\"local_epochs\"]\n", "\n", " # Use values provided by the config\n", - " print(f\"[Client {self.cid}, round {server_round}] fit, config: {config}\")\n", + " print(f\"[Client {self.pid}, round {server_round}] fit, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " train(self.net, self.trainloader, epochs=local_epochs)\n", " return get_parameters(self.net), len(self.trainloader), {}\n", "\n", " def evaluate(self, parameters, config):\n", - " print(f\"[Client {self.cid}] evaluate, config: {config}\")\n", + " print(f\"[Client {self.pid}] evaluate, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " loss, accuracy = test(self.net, self.valloader)\n", " return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}\n", "\n", "\n", - "def client_fn(cid) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerClient(cid, net, trainloader, valloader)" + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerClient(partition_id, net, trainloader, valloader).to_client()\n", + "\n", + "\n", + "# Create the ClientApp\n", + "client = ClientApp(client_fn=client_fn)" ] }, { @@ -490,7 +599,7 @@ " \"\"\"\n", " config = {\n", " \"server_round\": server_round, # The current round of federated learning\n", - " \"local_epochs\": 1 if server_round < 2 else 2, #\n", + " \"local_epochs\": 1 if server_round < 2 else 2,\n", " }\n", " return config" ] @@ -499,7 +608,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, we'll just pass this function to the FedAvg strategy before starting the simulation:" + "Next, we'll pass this function to the FedAvg strategy before starting the simulation:" ] }, { @@ -508,23 +617,31 @@ "metadata": {}, "outputs": [], "source": [ - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=0.3,\n", - " fraction_evaluate=0.3,\n", - " min_fit_clients=3,\n", - " min_evaluate_clients=3,\n", - " min_available_clients=NUM_CLIENTS,\n", - " initial_parameters=fl.common.ndarrays_to_parameters(get_parameters(Net())),\n", - " evaluate_fn=evaluate,\n", - " on_fit_config_fn=fit_config, # Pass the fit_config function\n", - ")\n", - "\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=3), # Just three rounds\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Create FedAvg strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=0.3,\n", + " fraction_evaluate=0.3,\n", + " min_fit_clients=3,\n", + " min_evaluate_clients=3,\n", + " min_available_clients=NUM_PARTITIONS,\n", + " initial_parameters=ndarrays_to_parameters(params),\n", + " evaluate_fn=evaluate,\n", + " on_fit_config_fn=fit_config, # Pass the fit_config function\n", + " )\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -552,16 +669,16 @@ "metadata": {}, "outputs": [], "source": [ - "NUM_CLIENTS = 1000\n", - "\n", - "trainloaders, valloaders, testloader = load_datasets(NUM_CLIENTS)" + "NUM_PARTITIONS = 1000" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now have 1000 partitions, each holding 45 training and 5 validation examples. Given that the number of training examples on each client is quite small, we should probably train the model a bit longer, so we configure the clients to perform 3 local training epochs. We should also adjust the fraction of clients selected for training during each round (we don't want all 1000 clients participating in every round), so we adjust `fraction_fit` to `0.05`, which means that only 5% of available clients (so 50 clients) will be selected for training each round:\n" + "Note that we can reuse the `ClientApp` for different `num-partitions` since the Context is defined by the `num_supernodes` argument in `run_simulation()`. \n", + "\n", + "We now have 1000 partitions, each holding 45 training and 5 validation examples. Given that the number of training examples on each client is quite small, we should probably train the model a bit longer, so we configure the clients to perform 3 local training epochs. We should also adjust the fraction of clients selected for training during each round (we don't want all 1000 clients participating in every round), so we adjust `fraction_fit` to `0.025`, which means that only 2.5% of available clients (so 25 clients) will be selected for training each round:\n" ] }, { @@ -578,22 +695,30 @@ " return config\n", "\n", "\n", - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=0.025, # Train on 25 clients (each round)\n", - " fraction_evaluate=0.05, # Evaluate on 50 clients (each round)\n", - " min_fit_clients=20,\n", - " min_evaluate_clients=40,\n", - " min_available_clients=NUM_CLIENTS,\n", - " initial_parameters=fl.common.ndarrays_to_parameters(get_parameters(Net())),\n", - " on_fit_config_fn=fit_config,\n", - ")\n", - "\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=NUM_CLIENTS,\n", - " config=fl.server.ServerConfig(num_rounds=3), # Just three rounds\n", - " strategy=strategy,\n", - " client_resources=client_resources,\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Create FedAvg strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=0.025, # Train on 25 clients (each round)\n", + " fraction_evaluate=0.05, # Evaluate on 50 clients (each round)\n", + " min_fit_clients=20,\n", + " min_evaluate_clients=40,\n", + " min_available_clients=NUM_PARTITIONS,\n", + " initial_parameters=ndarrays_to_parameters(params),\n", + " on_fit_config_fn=fit_config,\n", + " )\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -614,7 +739,7 @@ "source": [ "## Next steps\n", "\n", - "Before you continue, make sure to join the Flower community on Slack: [Join Slack](https://flower.ai/join-slack/)\n", + "Before you continue, make sure to join the Flower community on Flower Discuss ([Join Flower Discuss](https://discuss.flower.ai)) and on Slack ([Join Slack](https://flower.ai/join-slack/)).\n", "\n", "There's a dedicated `#questions` channel if you need help, but we'd also love to hear who you are in `#introductions`!\n", "\n", From 850038cfd2e44fc75925ab76d996fdfbcd607dce Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 26 Jul 2024 16:47:28 +0100 Subject: [PATCH 011/188] refactor(examples) Update `quickstart-mlx` example (#3701) Co-authored-by: Charles Beauville Co-authored-by: Daniel J. Beutel Co-authored-by: Chong Shen Ng --- examples/quickstart-mlx/README.md | 362 ++---------------- examples/quickstart-mlx/client.py | 152 -------- .../quickstart-mlx/mlxexample/__init__.py | 1 + .../quickstart-mlx/mlxexample/client_app.py | 72 ++++ .../quickstart-mlx/mlxexample/server_app.py | 49 +++ examples/quickstart-mlx/mlxexample/task.py | 101 +++++ examples/quickstart-mlx/pyproject.toml | 50 ++- examples/quickstart-mlx/requirements.txt | 4 - examples/quickstart-mlx/run.sh | 17 - examples/quickstart-mlx/server.py | 25 -- 10 files changed, 297 insertions(+), 536 deletions(-) delete mode 100644 examples/quickstart-mlx/client.py create mode 100644 examples/quickstart-mlx/mlxexample/__init__.py create mode 100644 examples/quickstart-mlx/mlxexample/client_app.py create mode 100644 examples/quickstart-mlx/mlxexample/server_app.py create mode 100644 examples/quickstart-mlx/mlxexample/task.py delete mode 100644 examples/quickstart-mlx/requirements.txt delete mode 100755 examples/quickstart-mlx/run.sh delete mode 100644 examples/quickstart-mlx/server.py diff --git a/examples/quickstart-mlx/README.md b/examples/quickstart-mlx/README.md index a4ac44bf8460..95b9ccf605b5 100644 --- a/examples/quickstart-mlx/README.md +++ b/examples/quickstart-mlx/README.md @@ -1,358 +1,70 @@ --- -title: Simple Flower Example using MLX +title: Federated Learning with MLX and Flower (Quickstart Example) tags: [quickstart, vision] dataset: [MNIST] framework: [MLX] --- -# Flower Example using MLX +# Federated Learning with MLX and Flower (Quickstart Example) -This introductory example to Flower uses [MLX](https://ml-explore.github.io/mlx/build/html/index.html), but deep knowledge of MLX is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. Running this example in itself is quite easy. +This introductory example to Flower uses [MLX](https://ml-explore.github.io/mlx/build/html/index.html), but you don't need deep knowledge of MLX to run it. The example will help you understand how to adapt Flower to your specific use case, and running it is quite straightforward. -[MLX](https://ml-explore.github.io/mlx/build/html/index.html) is a NumPy-like array framework designed for efficient and flexible machine learning on Apple silicon. +[MLX](https://ml-explore.github.io/mlx/build/html/index.html) is a NumPy-like array framework designed for efficient and flexible machine learning on Apple Silicon. In this example, we will train a simple 2-layer MLP on the [MNIST](https://huggingface.co/datasets/ylecun/mnist) dataset (handwritten digits recognition). The data will be downloaded and partitioned using [Flower Datasets](https://flower.ai/docs/datasets/). -In this example, we will train a simple 2 layers MLP on MNIST data (handwritten digits recognition). +## Set up the project -## Project Setup +### Clone the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +Start by cloning the example project: ```shell -git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/quickstart-mlx . && rm -rf _tmp && cd quickstart-mlx +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-mlx . \ + && rm -rf _tmp \ + && cd quickstart-mlx ``` -This will create a new directory called `quickstart-mlx` containing the following files: +This will create a new directory called `quickstart-mlx` with the following structure: ```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- run.sh --- README.md +quickstart-mlx +β”œβ”€β”€ mlxexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies +### Install dependencies and project -Project dependencies (such as `mlx` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +Install the dependencies defined in `pyproject.toml` as well as the `mlxexample` package. -#### Poetry - -```shell -poetry install -poetry shell -``` - -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python3 -c "import flwr" +```bash +pip install -e . ``` -If you don't see any errors you're good to go! +## Run the project -#### pip +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +### Run with the Simulation Engine -```shell -pip install -r requirements.txt +```bash +flwr run . ``` -## Run Federated Learning with MLX and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 server.py +```bash +flwr run . --run-config num-server-rounds=5,learning-rate=0.05 ``` -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminal windows and run the -following commands. - -Start a first client in the first terminal: - -```shell -python3 client.py --partition-id 0 -``` - -And another one in the second terminal: - -```shell -python3 client.py --partition-id 1 -``` - -If you want to utilize your GPU, you can use the `--gpu` argument: - -```shell -python3 client.py --gpu --partition-id 2 -``` - -Note that you can start many more clients if you want, but each will have to be in its own terminal. - -You will see that MLX is starting a federated training. Look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-mlx) for a detailed explanation. - -## Explanations - -This example is a federated version of the centralized case that can be found -[here](https://github.com/ml-explore/mlx-examples/tree/main/mnist). - -### The data - -We will use `flwr_datasets` to easily download and partition the `MNIST` dataset: - -```python -fds = FederatedDataset(dataset="mnist", partitioners={"train": 3}) -partition = fds.load_partition(partition_id = args.partition_id) -partition_splits = partition.train_test_split(test_size=0.2) - -partition_splits['train'].set_format("numpy") -partition_splits['test'].set_format("numpy") - -train_partition = partition_splits["train"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", -) -test_partition = partition_splits["test"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", -) - -data = ( - train_partition["img"], - train_partition["label"].astype(np.uint32), - test_partition["img"], - test_partition["label"].astype(np.uint32), -) - -train_images, train_labels, test_images, test_labels = map(mlx.core.array, data) -``` - -### The model - -We define the model as in the centralized mlx example, it's a simple MLP: - -```python -class MLP(mlx.nn.Module): - """A simple MLP.""" - - def __init__( - self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int - ): - super().__init__() - layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] - self.layers = [ - mlx.nn.Linear(idim, odim) - for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) - ] - - def __call__(self, x): - for l in self.layers[:-1]: - x = mlx.core.maximum(l(x), 0.0) - return self.layers[-1](x) - -``` - -We also define some utility functions to test our model and to iterate over batches. - -```python -def loss_fn(model, X, y): - return mlx.core.mean(mlx.nn.losses.cross_entropy(model(X), y)) - - -def eval_fn(model, X, y): - return mlx.core.mean(mlx.core.argmax(model(X), axis=1) == y) - +> \[!TIP\] +> For a more detailed walk-through check our [quickstart MLX tutorial](https://flower.ai/docs/framework/tutorial-quickstart-mlx.html) -def batch_iterate(batch_size, X, y): - perm = mlx.core.array(np.random.permutation(y.size)) - for s in range(0, y.size, batch_size): - ids = perm[s : s + batch_size] - yield X[ids], y[ids] +### Run with the Deployment Engine -``` - -### The client - -The main changes we have to make to use `MLX` with `Flower` will be found in -the `get_parameters` and `set_parameters` functions. Indeed, MLX doesn't -provide an easy way to convert the model parameters into a list of `np.array`s -(the format we need for the serialization of the messages to work). - -The way MLX stores its parameters is as follows: - -``` -{ - "layers": [ - {"weight": mlx.core.array, "bias": mlx.core.array}, - {"weight": mlx.core.array, "bias": mlx.core.array}, - ..., - {"weight": mlx.core.array, "bias": mlx.core.array} - ] -} -``` - -Therefore, to get our list of `np.array`s, we need to extract each array and -convert them into a numpy array: - -```python -def get_parameters(self, config): - layers = self.model.parameters()["layers"] - return [np.array(val) for layer in layers for _, val in layer.items()] -``` - -For the `set_parameters` function, we perform the reverse operation. We receive -a list of arrays and want to convert them into MLX parameters. Therefore, we -iterate through pairs of parameters and assign them to the `weight` and `bias` -keys of each layer dict: - -```python -def set_parameters(self, parameters): - new_params = {} - new_params["layers"] = [ - {"weight": mlx.core.array(parameters[i]), "bias": mlx.core.array(parameters[i + 1])} - for i in range(0, len(parameters), 2) - ] - self.model.update(new_params) -``` - -The rest of the functions are directly inspired by the centralized case: - -```python -def fit(self, parameters, config): - self.set_parameters(parameters) - for _ in range(self.num_epochs): - for X, y in batch_iterate( - self.batch_size, self.train_images, self.train_labels - ): - loss, grads = self.loss_and_grad_fn(self.model, X, y) - self.optimizer.update(self.model, grads) - mlx.core.eval(self.model.parameters(), self.optimizer.state) - return self.get_parameters(config={}), len(self.train_images), {} -``` - -Here, after updating the parameters, we perform the training as in the -centralized case, and return the new parameters. - -And for the `evaluate` function: - -```python -def evaluate(self, parameters, config): - self.set_parameters(parameters) - accuracy = eval_fn(self.model, self.test_images, self.test_labels) - loss = loss_fn(self.model, self.test_images, self.test_labels) - return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} -``` - -We also begin by updating the parameters with the ones sent by the server, and -then we compute the loss and accuracy using the functions defined above. - -Putting everything together we have: - -```python -class FlowerClient(fl.client.NumPyClient): - def __init__( - self, model, optim, loss_and_grad_fn, data, num_epochs, batch_size - ) -> None: - self.model = model - self.optimizer = optim - self.loss_and_grad_fn = loss_and_grad_fn - self.train_images, self.train_labels, self.test_images, self.test_labels = data - self.num_epochs = num_epochs - self.batch_size = batch_size - - def get_parameters(self, config): - layers = self.model.parameters()["layers"] - return [np.array(val) for layer in layers for _, val in layer.items()] - - def set_parameters(self, parameters): - new_params = {} - new_params["layers"] = [ - {"weight": mlx.core.array(parameters[i]), "bias": mlx.core.array(parameters[i + 1])} - for i in range(0, len(parameters), 2) - ] - self.model.update(new_params) - - def fit(self, parameters, config): - self.set_parameters(parameters) - for _ in range(self.num_epochs): - for X, y in batch_iterate( - self.batch_size, self.train_images, self.train_labels - ): - loss, grads = self.loss_and_grad_fn(self.model, X, y) - self.optimizer.update(self.model, grads) - mlx.core.eval(self.model.parameters(), self.optimizer.state) - return self.get_parameters(config={}), len(self.train_images), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - accuracy = eval_fn(self.model, self.test_images, self.test_labels) - loss = loss_fn(self.model, self.test_images, self.test_labels) - return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} -``` - -And as you can see, with only a few lines of code, our client is ready! Before -we can instantiate it, we need to define a few variables: - -```python -num_layers = 2 -hidden_dim = 32 -num_classes = 10 -batch_size = 256 -num_epochs = 1 -learning_rate = 1e-1 - -model = MLP(num_layers, train_images.shape[-1], hidden_dim, num_classes) - -loss_and_grad_fn = mlx.nn.value_and_grad(model, loss_fn) -optimizer = mlx.optimizers.SGD(learning_rate=learning_rate) -``` - -Finally, we can instantiate it by using the `start_client` function: - -```python -# Start Flower client -fl.client.start_client( - server_address="127.0.0.1:8080", - client=FlowerClient( - model, - optimizer, - loss_and_grad_fn, - (train_images, train_labels, test_images, test_labels), - num_epochs, - batch_size, - ).to_client(), -) -``` - -### The server - -On the server side, we don't need to add anything in particular. The -`weighted_average` function is just there to be able to aggregate the results -and have an accuracy at the end. - -```python -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-mlx/client.py b/examples/quickstart-mlx/client.py deleted file mode 100644 index 344cfc65e42d..000000000000 --- a/examples/quickstart-mlx/client.py +++ /dev/null @@ -1,152 +0,0 @@ -import argparse - -import mlx.core as mx -import mlx.nn as nn -import mlx.optimizers as optim -import numpy as np -from flwr_datasets import FederatedDataset - -import flwr as fl - - -class MLP(nn.Module): - """A simple MLP.""" - - def __init__( - self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int - ): - super().__init__() - layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] - self.layers = [ - nn.Linear(idim, odim) - for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) - ] - - def __call__(self, x): - for l in self.layers[:-1]: - x = mx.maximum(l(x), 0.0) - return self.layers[-1](x) - - -def loss_fn(model, X, y): - return mx.mean(nn.losses.cross_entropy(model(X), y)) - - -def eval_fn(model, X, y): - return mx.mean(mx.argmax(model(X), axis=1) == y) - - -def batch_iterate(batch_size, X, y): - perm = mx.array(np.random.permutation(y.size)) - for s in range(0, y.size, batch_size): - ids = perm[s : s + batch_size] - yield X[ids], y[ids] - - -# Define Flower client -class FlowerClient(fl.client.NumPyClient): - def __init__( - self, model, optim, loss_and_grad_fn, data, num_epochs, batch_size - ) -> None: - self.model = model - self.optimizer = optim - self.loss_and_grad_fn = loss_and_grad_fn - self.train_images, self.train_labels, self.test_images, self.test_labels = data - self.num_epochs = num_epochs - self.batch_size = batch_size - - def get_parameters(self, config): - layers = self.model.parameters()["layers"] - return [np.array(val) for layer in layers for _, val in layer.items()] - - def set_parameters(self, parameters): - new_params = {} - new_params["layers"] = [ - {"weight": mx.array(parameters[i]), "bias": mx.array(parameters[i + 1])} - for i in range(0, len(parameters), 2) - ] - self.model.update(new_params) - - def fit(self, parameters, config): - self.set_parameters(parameters) - for _ in range(self.num_epochs): - for X, y in batch_iterate( - self.batch_size, self.train_images, self.train_labels - ): - loss, grads = self.loss_and_grad_fn(self.model, X, y) - self.optimizer.update(self.model, grads) - mx.eval(self.model.parameters(), self.optimizer.state) - return self.get_parameters(config={}), len(self.train_images), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - accuracy = eval_fn(self.model, self.test_images, self.test_labels) - loss = loss_fn(self.model, self.test_images, self.test_labels) - return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} - - -if __name__ == "__main__": - parser = argparse.ArgumentParser("Train a simple MLP on MNIST with MLX.") - parser.add_argument("--gpu", action="store_true", help="Use the Metal back-end.") - parser.add_argument( - "--partition-id", - choices=[0, 1, 2], - type=int, - help="Partition of the dataset divided into 3 iid partitions created artificially.", - ) - args = parser.parse_args() - if not args.gpu: - mx.set_default_device(mx.cpu) - - num_layers = 2 - hidden_dim = 32 - num_classes = 10 - batch_size = 256 - num_epochs = 1 - learning_rate = 1e-1 - - fds = FederatedDataset(dataset="mnist", partitioners={"train": 3}) - partition = fds.load_partition(partition_id=args.partition_id) - partition_splits = partition.train_test_split(test_size=0.2, seed=42) - - partition_splits["train"].set_format("numpy") - partition_splits["test"].set_format("numpy") - - train_partition = partition_splits["train"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", - ) - test_partition = partition_splits["test"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", - ) - - data = ( - train_partition["img"], - train_partition["label"].astype(np.uint32), - test_partition["img"], - test_partition["label"].astype(np.uint32), - ) - - train_images, train_labels, test_images, test_labels = map(mx.array, data) - model = MLP(num_layers, train_images.shape[-1], hidden_dim, num_classes) - - loss_and_grad_fn = nn.value_and_grad(model, loss_fn) - optimizer = optim.SGD(learning_rate=learning_rate) - - # Start Flower client - fl.client.start_client( - server_address="127.0.0.1:8080", - client=FlowerClient( - model, - optimizer, - loss_and_grad_fn, - (train_images, train_labels, test_images, test_labels), - num_epochs, - batch_size, - ).to_client(), - ) diff --git a/examples/quickstart-mlx/mlxexample/__init__.py b/examples/quickstart-mlx/mlxexample/__init__.py new file mode 100644 index 000000000000..d4bb5d0d511d --- /dev/null +++ b/examples/quickstart-mlx/mlxexample/__init__.py @@ -0,0 +1 @@ +"""mlxexample: A Flower / MLX app.""" diff --git a/examples/quickstart-mlx/mlxexample/client_app.py b/examples/quickstart-mlx/mlxexample/client_app.py new file mode 100644 index 000000000000..f1ea7bce65d8 --- /dev/null +++ b/examples/quickstart-mlx/mlxexample/client_app.py @@ -0,0 +1,72 @@ +"""mlxexample: A Flower / MLX app.""" + +import mlx.core as mx +import mlx.nn as nn +import mlx.optimizers as optim +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context +from mlxexample.task import ( + MLP, + batch_iterate, + eval_fn, + get_params, + load_data, + loss_fn, + set_params, +) + + +class FlowerClient(NumPyClient): + def __init__(self, model, optimizer, batch_size, data): + self.train_images, self.train_labels, self.test_images, self.test_labels = data + self.model = model + self.optimizer = optimizer + self.num_epochs = 1 + self.batch_size = batch_size + + def fit(self, parameters, config): + """Train the model with data of this client.""" + set_params(self.model, parameters) + loss_and_grad_fn = nn.value_and_grad(self.model, loss_fn) + for _ in range(self.num_epochs): + for X, y in batch_iterate( + self.batch_size, self.train_images, self.train_labels + ): + _, grads = loss_and_grad_fn(self.model, X, y) + self.optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), self.optimizer.state) + return get_params(self.model), len(self.train_images), {} + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_params(self.model, parameters) + accuracy = eval_fn(self.model, self.test_images, self.test_labels) + loss = loss_fn(self.model, self.test_images, self.test_labels) + return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + data = load_data(partition_id, num_partitions) + + # Read the run config to get settings to configure the Client + num_layers = context.run_config["num-layers"] + hidden_dim = context.run_config["hidden-dim"] + img_size = context.run_config["img-size"] + batch_size = context.run_config["batch-size"] + lr = context.run_config["learning-rate"] + + # Prepare model and optimizer + model = MLP(num_layers, img_size**2, hidden_dim) + optimizer = optim.SGD(learning_rate=lr) + + # Return Client instance + return FlowerClient(model, optimizer, batch_size, data).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-mlx/mlxexample/server_app.py b/examples/quickstart-mlx/mlxexample/server_app.py new file mode 100644 index 000000000000..7a50dfd41305 --- /dev/null +++ b/examples/quickstart-mlx/mlxexample/server_app.py @@ -0,0 +1,49 @@ +"""mlxexample: A Flower / MLX app.""" + +from typing import List, Tuple + +from flwr.common import Context, Metrics, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from mlxexample.task import MLP, get_params + + +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + """Aggregate custom `accuracy` metric by weighted average.""" + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components that set the ServerApp behaviour.""" + + # Init model + model = MLP( + num_layers=context.run_config["num-layers"], + input_dim=context.run_config["img-size"] ** 2, + hidden_dim=context.run_config["hidden-dim"], + ) + + # Convert model parameters to flwr.common.Parameters + ndarrays = get_params(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define the strategy + fraction_eval = context.run_config["fraction-evaluate"] + strategy = FedAvg( + fraction_evaluate=fraction_eval, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=global_model_init, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-mlx/mlxexample/task.py b/examples/quickstart-mlx/mlxexample/task.py new file mode 100644 index 000000000000..9197643aa6bc --- /dev/null +++ b/examples/quickstart-mlx/mlxexample/task.py @@ -0,0 +1,101 @@ +"""mlxexample: A Flower / MLX app.""" + +import mlx.core as mx +import mlx.nn as nn +import numpy as np +from datasets.utils.logging import disable_progress_bar +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + +disable_progress_bar() + + +class MLP(nn.Module): + """A simple MLP.""" + + def __init__( + self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int = 10 + ): + super().__init__() + layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] + self.layers = [ + nn.Linear(idim, odim) + for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) + ] + + def __call__(self, x): + for l in self.layers[:-1]: + x = mx.maximum(l(x), 0.0) + return self.layers[-1](x) + + +def get_params(model): + layers = model.parameters()["layers"] + return [np.array(val) for layer in layers for _, val in layer.items()] + + +def set_params(model, parameters): + new_params = {} + new_params["layers"] = [ + {"weight": mx.array(parameters[i]), "bias": mx.array(parameters[i + 1])} + for i in range(0, len(parameters), 2) + ] + model.update(new_params) + + +def loss_fn(model, X, y): + return mx.mean(nn.losses.cross_entropy(model(X), y)) + + +def eval_fn(model, X, y): + return mx.mean(mx.argmax(model(X), axis=1) == y) + + +def batch_iterate(batch_size, X, y): + perm = mx.array(np.random.permutation(y.size)) + for s in range(0, y.size, batch_size): + ids = perm[s : s + batch_size] + yield X[ids], y[ids] + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + trust_remote_code=True, + ) + partition = fds.load_partition(partition_id) + partition_splits = partition.train_test_split(test_size=0.2, seed=42) + + partition_splits["train"].set_format("numpy") + partition_splits["test"].set_format("numpy") + + train_partition = partition_splits["train"].map( + lambda img: { + "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 + }, + input_columns="image", + ) + test_partition = partition_splits["test"].map( + lambda img: { + "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 + }, + input_columns="image", + ) + + data = ( + train_partition["img"], + train_partition["label"].astype(np.uint32), + test_partition["img"], + test_partition["label"].astype(np.uint32), + ) + + train_images, train_labels, test_images, test_labels = map(mx.array, data) + return train_images, train_labels, test_images, test_labels diff --git a/examples/quickstart-mlx/pyproject.toml b/examples/quickstart-mlx/pyproject.toml index 752040b6aaa9..36e39bcd6d78 100644 --- a/examples/quickstart-mlx/pyproject.toml +++ b/examples/quickstart-mlx/pyproject.toml @@ -1,16 +1,40 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "quickstart-mlx" -version = "0.1.0" -description = "MLX Federated Learning Quickstart with Flower" -authors = ["The Flower Authors "] +[project] +name = "mlxexample" +version = "1.0.0" +description = "Federated Learning with MLX and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "mlx==0.16.0", + "numpy==1.26.4", +] -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -mlx = "==0.0.3" -numpy = "==1.24.4" -flwr-datasets = { extras = ["vision"], version = "^0.0.2" } +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "mlxexample.server_app:app" +clientapp = "mlxexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +fraction-evaluate = 0.5 +num-layers = 2 +img-size = 28 +hidden-dim = 32 +batch-size = 256 +learning-rate = 0.1 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/quickstart-mlx/requirements.txt b/examples/quickstart-mlx/requirements.txt deleted file mode 100644 index b56f7a15bfb9..000000000000 --- a/examples/quickstart-mlx/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr>=1.0, <2.0 -mlx==0.0.3 -numpy==1.24.4 -flwr-datasets[vision]>=0.0.2, <1.0.0 diff --git a/examples/quickstart-mlx/run.sh b/examples/quickstart-mlx/run.sh deleted file mode 100755 index 40d211848c07..000000000000 --- a/examples/quickstart-mlx/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in $(seq 0 1); do - echo "Starting client $i" - python client.py --partition-id $i & -done - -# Enable CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-mlx/server.py b/examples/quickstart-mlx/server.py deleted file mode 100644 index fe691a88aba0..000000000000 --- a/examples/quickstart-mlx/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List, Tuple - -import flwr as fl -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) From 159d248c8864f1fc648b6890ac52ccbfb9f910a9 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 26 Jul 2024 17:01:51 +0100 Subject: [PATCH 012/188] docs(framework) Update Flower tutorial series to Flower Next (part 3 of 4) (#3314) --- ...uild-a-strategy-from-scratch-pytorch.ipynb | 174 ++++++++++-------- 1 file changed, 100 insertions(+), 74 deletions(-) diff --git a/doc/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb b/doc/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb index c5fc777e7f26..803ce729d43b 100644 --- a/doc/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb +++ b/doc/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb @@ -7,13 +7,15 @@ "source": [ "# Build a strategy from scratch\n", "\n", - "Welcome to the third part of the Flower federated learning tutorial. In previous parts of this tutorial, we introduced federated learning with PyTorch and Flower ([part 1](https://flower.ai/docs/framework/tutorial-get-started-with-flower-pytorch.html)) and we learned how strategies can be used to customize the execution on both the server and the clients ([part 2](https://flower.ai/docs/framework/tutorial-use-a-federated-learning-strategy-pytorch.html)).\n", + "Welcome to the third part of the Flower federated learning tutorial. In previous parts of this tutorial, we introduced federated learning with PyTorch and the Flower framework ([part 1](https://flower.ai/docs/framework/tutorial-get-started-with-flower-pytorch.html)) and we learned how strategies can be used to customize the execution on both the server and the clients ([part 2](https://flower.ai/docs/framework/tutorial-use-a-federated-learning-strategy-pytorch.html)).\n", "\n", - "In this notebook, we'll continue to customize the federated learning system we built previously by creating a custom version of FedAvg (again, using [Flower](https://flower.ai/) and [PyTorch](https://pytorch.org/)).\n", + "In this notebook, we'll continue to customize the federated learning system we built previously by creating a custom version of FedAvg using the Flower framework, Flower Datasets, and PyTorch.\n", "\n", - "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Slack to connect, ask questions, and get help: [Join Slack](https://flower.ai/join-slack) 🌼 We'd love to hear from you in the `#introductions` channel! And if anything is unclear, head over to the `#questions` channel.\n", + "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Flower Discuss and the Flower Slack to connect, ask questions, and get help:\n", + "> - [Join Flower Discuss](https://discuss.flower.ai/) We'd love to hear from you in the `Introduction` topic! If anything is unclear, post in `Flower Help - Beginners`.\n", + "> - [Join Flower Slack](https://flower.ai/join-slack) We'd love to hear from you in the `#introductions` channel! If anything is unclear, head over to the `#questions` channel.\n", "\n", - "Let's build a new `Strategy` from scratch!" + "Let's build a new `Strategy` from scratch! 🌼" ] }, { @@ -40,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install -q flwr[simulation] torch torchvision" + "!pip install -q flwr[simulation] flwr-datasets[vision] torch torchvision" ] }, { @@ -64,15 +66,19 @@ "import torch.nn as nn\n", "import torch.nn.functional as F\n", "import torchvision.transforms as transforms\n", - "from torch.utils.data import DataLoader, random_split\n", - "from torchvision.datasets import CIFAR10\n", + "from torch.utils.data import DataLoader\n", "\n", - "import flwr as fl\n", + "import flwr\n", + "from flwr.client import Client, ClientApp, NumPyClient\n", + "from flwr.common import Context\n", + "from flwr.server import ServerApp, ServerConfig, ServerAppComponents\n", + "from flwr.server.strategy import Strategy\n", + "from flwr.simulation import run_simulation\n", + "from flwr_datasets import FederatedDataset\n", "\n", "DEVICE = torch.device(\"cpu\") # Try \"cuda\" to train on GPU\n", - "print(\n", - " f\"Training on {DEVICE} using PyTorch {torch.__version__} and Flower {fl.__version__}\"\n", - ")" + "print(f\"Training on {DEVICE}\")\n", + "print(f\"Flower {flwr.__version__} / PyTorch {torch.__version__}\")" ] }, { @@ -88,7 +94,7 @@ "source": [ "### Data loading\n", "\n", - "Let's now load the CIFAR-10 training and test set, partition them into ten smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`. We introduce a new parameter `num_clients` which allows us to call `load_datasets` with different numbers of clients." + "Let's now load the CIFAR-10 training and test set, partition them into ten smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`." ] }, { @@ -97,37 +103,28 @@ "metadata": {}, "outputs": [], "source": [ - "NUM_CLIENTS = 10\n", - "\n", - "\n", - "def load_datasets(num_clients: int):\n", - " # Download and transform CIFAR-10 (train and test)\n", - " transform = transforms.Compose(\n", + "def load_datasets(partition_id, num_partitions: int):\n", + " fds = FederatedDataset(dataset=\"cifar10\", partitioners={\"train\": num_partitions})\n", + " partition = fds.load_partition(partition_id)\n", + " # Divide data on each node: 80% train, 20% test\n", + " partition_train_test = partition.train_test_split(test_size=0.2, seed=42)\n", + " pytorch_transforms = transforms.Compose(\n", " [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]\n", " )\n", - " trainset = CIFAR10(\"./dataset\", train=True, download=True, transform=transform)\n", - " testset = CIFAR10(\"./dataset\", train=False, download=True, transform=transform)\n", - "\n", - " # Split training set into `num_clients` partitions to simulate different local datasets\n", - " partition_size = len(trainset) // num_clients\n", - " lengths = [partition_size] * num_clients\n", - " datasets = random_split(trainset, lengths, torch.Generator().manual_seed(42))\n", - "\n", - " # Split each partition into train/val and create DataLoader\n", - " trainloaders = []\n", - " valloaders = []\n", - " for ds in datasets:\n", - " len_val = len(ds) // 10 # 10 % validation set\n", - " len_train = len(ds) - len_val\n", - " lengths = [len_train, len_val]\n", - " ds_train, ds_val = random_split(ds, lengths, torch.Generator().manual_seed(42))\n", - " trainloaders.append(DataLoader(ds_train, batch_size=32, shuffle=True))\n", - " valloaders.append(DataLoader(ds_val, batch_size=32))\n", - " testloader = DataLoader(testset, batch_size=32)\n", - " return trainloaders, valloaders, testloader\n", - "\n", "\n", - "trainloaders, valloaders, testloader = load_datasets(NUM_CLIENTS)" + " def apply_transforms(batch):\n", + " # Instead of passing transforms to CIFAR10(..., transform=transform)\n", + " # we will use this function to dataset.with_transform(apply_transforms)\n", + " # The transforms object is exactly the same\n", + " batch[\"img\"] = [pytorch_transforms(img) for img in batch[\"img\"]]\n", + " return batch\n", + "\n", + " partition_train_test = partition_train_test.with_transform(apply_transforms)\n", + " trainloader = DataLoader(partition_train_test[\"train\"], batch_size=32, shuffle=True)\n", + " valloader = DataLoader(partition_train_test[\"test\"], batch_size=32)\n", + " testset = fds.load_split(\"test\").with_transform(apply_transforms)\n", + " testloader = DataLoader(testset, batch_size=32)\n", + " return trainloader, valloader, testloader" ] }, { @@ -182,7 +179,8 @@ " net.train()\n", " for epoch in range(epochs):\n", " correct, total, epoch_loss = 0, 0, 0.0\n", - " for images, labels in trainloader:\n", + " for batch in trainloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " optimizer.zero_grad()\n", " outputs = net(images)\n", @@ -204,7 +202,8 @@ " correct, total, loss = 0, 0, 0.0\n", " net.eval()\n", " with torch.no_grad():\n", - " for images, labels in testloader:\n", + " for batch in testloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " outputs = net(images)\n", " loss += criterion(outputs, labels).item()\n", @@ -222,7 +221,7 @@ "source": [ "### Flower client\n", "\n", - "To implement the Flower client, we (again) create a subclass of `flwr.client.NumPyClient` and implement the three methods `get_parameters`, `fit`, and `evaluate`. Here, we also pass the `cid` to the client and use it log additional details:" + "To implement the Flower client, we (again) create a subclass of `flwr.client.NumPyClient` and implement the three methods `get_parameters`, `fit`, and `evaluate`. Here, we also pass the `partition_id` to the client and use it log additional details. We then create an instance of `ClientApp` and pass it the `client_fn`." ] }, { @@ -231,35 +230,40 @@ "metadata": {}, "outputs": [], "source": [ - "class FlowerClient(fl.client.NumPyClient):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerClient(NumPyClient):\n", + " def __init__(self, partition_id, net, trainloader, valloader):\n", + " self.partition_id = partition_id\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, config):\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.partition_id}] get_parameters\")\n", " return get_parameters(self.net)\n", "\n", " def fit(self, parameters, config):\n", - " print(f\"[Client {self.cid}] fit, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] fit, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " train(self.net, self.trainloader, epochs=1)\n", " return get_parameters(self.net), len(self.trainloader), {}\n", "\n", " def evaluate(self, parameters, config):\n", - " print(f\"[Client {self.cid}] evaluate, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] evaluate, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " loss, accuracy = test(self.net, self.valloader)\n", " return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}\n", "\n", "\n", - "def client_fn(cid) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerClient(cid, net, trainloader, valloader)" + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerClient(partition_id, net, trainloader, valloader).to_client()\n", + "\n", + "\n", + "# Create the ClientApp\n", + "client = ClientApp(client_fn=client_fn)" ] }, { @@ -275,16 +279,31 @@ "metadata": {}, "outputs": [], "source": [ - "# Specify client resources if you need GPU (defaults to 1 CPU and 0 GPU)\n", - "client_resources = None\n", - "if DEVICE.type == \"cuda\":\n", - " client_resources = {\"num_gpus\": 1}\n", + "NUM_PARTITIONS = 10\n", "\n", - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=2,\n", - " config=fl.server.ServerConfig(num_rounds=3),\n", - " client_resources=client_resources,\n", + "\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Configure the server for just 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " # If no strategy is provided, by default, ServerAppComponents will use FedAvg\n", + " return ServerAppComponents(config=config)\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)\n", + "\n", + "# Specify the resources each of your clients need\n", + "# If set to none, by default, each client will be allocated 2x CPU and 0x GPUs\n", + "backend_config = {\"client_resources\": None}\n", + "if DEVICE.type == \"cuda\":\n", + " backend_config = {\"client_resources\": {\"num_gpus\": 1}}\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -303,15 +322,13 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Callable, Union\n", + "from typing import Union\n", "\n", "from flwr.common import (\n", " EvaluateIns,\n", " EvaluateRes,\n", " FitIns,\n", " FitRes,\n", - " MetricsAggregationFn,\n", - " NDArrays,\n", " Parameters,\n", " Scalar,\n", " ndarrays_to_parameters,\n", @@ -322,7 +339,7 @@ "from flwr.server.strategy.aggregate import aggregate, weighted_loss_avg\n", "\n", "\n", - "class FedCustom(fl.server.strategy.Strategy):\n", + "class FedCustom(Strategy):\n", " def __init__(\n", " self,\n", " fraction_fit: float = 1.0,\n", @@ -347,7 +364,7 @@ " \"\"\"Initialize global model parameters.\"\"\"\n", " net = Net()\n", " ndarrays = get_parameters(net)\n", - " return fl.common.ndarrays_to_parameters(ndarrays)\n", + " return ndarrays_to_parameters(ndarrays)\n", "\n", " def configure_fit(\n", " self, server_round: int, parameters: Parameters, client_manager: ClientManager\n", @@ -465,12 +482,21 @@ "metadata": {}, "outputs": [], "source": [ - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=2,\n", - " config=fl.server.ServerConfig(num_rounds=3),\n", - " strategy=FedCustom(), # <-- pass the new strategy here\n", - " client_resources=client_resources,\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Configure the server for just 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(\n", + " config=config,\n", + " strategy=FedCustom(), # <-- pass the new strategy here\n", + " )\n", + "\n", + "\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -489,7 +515,7 @@ "source": [ "## Next steps\n", "\n", - "Before you continue, make sure to join the Flower community on Slack: [Join Slack](https://flower.ai/join-slack/)\n", + "Before you continue, make sure to join the Flower community on Flower Discuss ([Join Flower Discuss](https://discuss.flower.ai)) and on Slack ([Join Slack](https://flower.ai/join-slack/)).\n", "\n", "There's a dedicated `#questions` channel if you need help, but we'd also love to hear who you are in `#introductions`!\n", "\n", From 693ff06ea9cb1b63caed6ae60dc46c188dd3096f Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:12:55 +0200 Subject: [PATCH 013/188] docs(datasets) Fix Flower Datasets docs (#3929) --- datasets/doc/source/conf.py | 2 +- datasets/doc/source/tutorial-quickstart.ipynb | 7 ++++++- datasets/doc/source/tutorial-use-partitioners.ipynb | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/datasets/doc/source/conf.py b/datasets/doc/source/conf.py index 29840da4abc0..dcba63dd221c 100644 --- a/datasets/doc/source/conf.py +++ b/datasets/doc/source/conf.py @@ -111,7 +111,7 @@ def find_test_modules(package_path): # Sphinx redirects, implemented after the doc filename changes. # To prevent 404 errors and redirect to the new pages. redirects = { - "how-to-visualize-label-distribution.html": "tutorial-visualize-label-distribution.html", + "how-to-visualize-label-distribution": "tutorial-visualize-label-distribution.html", } # -- Options for HTML output ------------------------------------------------- diff --git a/datasets/doc/source/tutorial-quickstart.ipynb b/datasets/doc/source/tutorial-quickstart.ipynb index ca6700aa31a3..fe71bdf58567 100644 --- a/datasets/doc/source/tutorial-quickstart.ipynb +++ b/datasets/doc/source/tutorial-quickstart.ipynb @@ -391,8 +391,11 @@ "## Use with PyTorch/NumPy/TensorFlow\n", "\n", "For more detailed instructions, go to:\n", + "\n", "* [how-to-use-with-pytorch](https://flower.ai/docs/datasets/how-to-use-with-pytorch.html)\n", + "\n", "* [how-to-use-with-numpy](https://flower.ai/docs/datasets/how-to-use-with-numpy.html)\n", + "\n", "* [how-to-use-with-tensorflow](https://flower.ai/docs/datasets/how-to-use-with-tensorflow.html)" ] }, @@ -443,7 +446,9 @@ "cell_type": "markdown", "id": "b93678a5", "metadata": {}, - "source": "The `Dataloader` created this way does not return a `Tuple` when iterating over it but a `Dict` with the names of the columns as keys and features as values. Look below for an example." + "source": [ + "The `Dataloader` created this way does not return a `Tuple` when iterating over it but a `Dict` with the names of the columns as keys and features as values. Look below for an example." + ] }, { "cell_type": "code", diff --git a/datasets/doc/source/tutorial-use-partitioners.ipynb b/datasets/doc/source/tutorial-use-partitioners.ipynb index 72fabda1504e..30ff55ede91d 100644 --- a/datasets/doc/source/tutorial-use-partitioners.ipynb +++ b/datasets/doc/source/tutorial-use-partitioners.ipynb @@ -40,7 +40,7 @@ "source": [ "### `IidPartitioner` Creation\n", "\n", - "Let's create (instantiate) the most basic partitioner, [`IidPartitioner`](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.IidPartitioner.html#flwr_datasets.partitioner.IidPartitioner) and learn how it interacts with `FederatedDataset`." + "Let's create (instantiate) the most basic partitioner, [IidPartitioner](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.IidPartitioner.html#flwr_datasets.partitioner.IidPartitioner) and learn how it interacts with `FederatedDataset`." ] }, { @@ -165,7 +165,7 @@ "source": [ "#### Creating non-IID partitions: Use ``PathologicalPartitioner``\n", "\n", - "Now, we are going to create partitions that have only a subset of labels in each partition by using [`PathologicalPartitioner`](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.PathologicalPartitioner.html#flwr_datasets.partitioner.PathologicalPartitioner). In this scenario we have the exact control about the number of unique labels on each partition. The smaller the number is the more heterogenous the division gets. Let's have a look at how it works with `num_classes_per_partition=2`." + "Now, we are going to create partitions that have only a subset of labels in each partition by using [PathologicalPartitioner](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.PathologicalPartitioner.html#flwr_datasets.partitioner.PathologicalPartitioner). In this scenario we have the exact control about the number of unique labels on each partition. The smaller the number is the more heterogenous the division gets. Let's have a look at how it works with `num_classes_per_partition=2`." ] }, { @@ -261,7 +261,7 @@ "source": [ "#### Creating non-IID partitions: Use ``DirichletPartitioner``\n", "\n", - "With the [`DirichletParitioner`](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.DirichletPartitioner.html#flwr_datasets.partitioner.DirichletPartitioner), the primary tool for controlling heterogeneity is the `alpha` parameter; the smaller the value gets, the more heterogeneous the federated datasets are. Instead of choosing the exact number of classes on each partition, here we sample the probability distribution from the Dirichlet distribution, which tells how the samples associated with each class will be divided." + "With the [DirichletPartitioner](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.DirichletPartitioner.html#flwr_datasets.partitioner.DirichletPartitioner), the primary tool for controlling heterogeneity is the `alpha` parameter; the smaller the value gets, the more heterogeneous the federated datasets are. Instead of choosing the exact number of classes on each partition, here we sample the probability distribution from the Dirichlet distribution, which tells how the samples associated with each class will be divided." ] }, { From 947b23f2dbd4b3e526ae869b151ffaeedfcd0fdb Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 26 Jul 2024 17:28:54 +0100 Subject: [PATCH 014/188] docs(framework) Update Flower tutorial series to Flower Next (part 4 of 4) (#3316) --- ...-series-customize-the-client-pytorch.ipynb | 261 +++++++++++------- 1 file changed, 160 insertions(+), 101 deletions(-) diff --git a/doc/source/tutorial-series-customize-the-client-pytorch.ipynb b/doc/source/tutorial-series-customize-the-client-pytorch.ipynb index dbdd1094173c..0d1a926c339c 100644 --- a/doc/source/tutorial-series-customize-the-client-pytorch.ipynb +++ b/doc/source/tutorial-series-customize-the-client-pytorch.ipynb @@ -11,9 +11,11 @@ "\n", "In this notebook, we revisit `NumPyClient` and introduce a new baseclass for building clients, simply named `Client`. In previous parts of this tutorial, we've based our client on `NumPyClient`, a convenience class which makes it easy to work with machine learning libraries that have good NumPy interoperability. With `Client`, we gain a lot of flexibility that we didn't have before, but we'll also have to do a few things the we didn't have to do before.\n", "\n", - "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Slack to connect, ask questions, and get help: [Join Slack](https://flower.ai/join-slack) 🌼 We'd love to hear from you in the `#introductions` channel! And if anything is unclear, head over to the `#questions` channel.\n", + "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Flower Discuss and the Flower Slack to connect, ask questions, and get help:\n", + "> - [Join Flower Discuss](https://discuss.flower.ai/) We'd love to hear from you in the `Introduction` topic! If anything is unclear, post in `Flower Help - Beginners`.\n", + "> - [Join Flower Slack](https://flower.ai/join-slack) We'd love to hear from you in the `#introductions` channel! If anything is unclear, head over to the `#questions` channel.\n", "\n", - "Let's go deeper and see what it takes to move from `NumPyClient` to `Client`!" + "Let's go deeper and see what it takes to move from `NumPyClient` to `Client`! 🌼" ] }, { @@ -40,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install -q flwr[simulation] torch torchvision scipy" + "!pip install -q flwr[simulation] flwr-datasets[vision] torch torchvision scipy" ] }, { @@ -57,22 +59,25 @@ "outputs": [], "source": [ "from collections import OrderedDict\n", - "from typing import Dict, List, Optional, Tuple\n", + "from typing import List\n", "\n", "import numpy as np\n", "import torch\n", "import torch.nn as nn\n", "import torch.nn.functional as F\n", "import torchvision.transforms as transforms\n", - "from torch.utils.data import DataLoader, random_split\n", - "from torchvision.datasets import CIFAR10\n", + "from torch.utils.data import DataLoader\n", "\n", - "import flwr as fl\n", + "import flwr\n", + "from flwr.client import Client, ClientApp, NumPyClient\n", + "from flwr.common import Context\n", + "from flwr.server import ServerApp, ServerConfig, ServerAppComponents\n", + "from flwr.simulation import run_simulation\n", + "from flwr_datasets import FederatedDataset\n", "\n", "DEVICE = torch.device(\"cpu\") # Try \"cuda\" to train on GPU\n", - "print(\n", - " f\"Training on {DEVICE} using PyTorch {torch.__version__} and Flower {fl.__version__}\"\n", - ")" + "print(f\"Training on {DEVICE}\")\n", + "print(f\"Flower {flwr.__version__} / PyTorch {torch.__version__}\")" ] }, { @@ -88,7 +93,7 @@ "source": [ "### Data loading\n", "\n", - "Let's now load the CIFAR-10 training and test set, partition them into ten smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`." + "Let's now define a loading function for the CIFAR-10 training and test set, partition them into `num_partitions` smaller datasets (each split into training and validation set), and wrap everything in their own `DataLoader`." ] }, { @@ -97,37 +102,28 @@ "metadata": {}, "outputs": [], "source": [ - "NUM_CLIENTS = 10\n", - "\n", - "\n", - "def load_datasets(num_clients: int):\n", - " # Download and transform CIFAR-10 (train and test)\n", - " transform = transforms.Compose(\n", + "def load_datasets(partition_id: int, num_partitions: int):\n", + " fds = FederatedDataset(dataset=\"cifar10\", partitioners={\"train\": num_partitions})\n", + " partition = fds.load_partition(partition_id)\n", + " # Divide data on each node: 80% train, 20% test\n", + " partition_train_test = partition.train_test_split(test_size=0.2, seed=42)\n", + " pytorch_transforms = transforms.Compose(\n", " [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]\n", " )\n", - " trainset = CIFAR10(\"./dataset\", train=True, download=True, transform=transform)\n", - " testset = CIFAR10(\"./dataset\", train=False, download=True, transform=transform)\n", - "\n", - " # Split training set into `num_clients` partitions to simulate different local datasets\n", - " partition_size = len(trainset) // num_clients\n", - " lengths = [partition_size] * num_clients\n", - " datasets = random_split(trainset, lengths, torch.Generator().manual_seed(42))\n", - "\n", - " # Split each partition into train/val and create DataLoader\n", - " trainloaders = []\n", - " valloaders = []\n", - " for ds in datasets:\n", - " len_val = len(ds) // 10 # 10 % validation set\n", - " len_train = len(ds) - len_val\n", - " lengths = [len_train, len_val]\n", - " ds_train, ds_val = random_split(ds, lengths, torch.Generator().manual_seed(42))\n", - " trainloaders.append(DataLoader(ds_train, batch_size=32, shuffle=True))\n", - " valloaders.append(DataLoader(ds_val, batch_size=32))\n", - " testloader = DataLoader(testset, batch_size=32)\n", - " return trainloaders, valloaders, testloader\n", - "\n", "\n", - "trainloaders, valloaders, testloader = load_datasets(NUM_CLIENTS)" + " def apply_transforms(batch):\n", + " # Instead of passing transforms to CIFAR10(..., transform=transform)\n", + " # we will use this function to dataset.with_transform(apply_transforms)\n", + " # The transforms object is exactly the same\n", + " batch[\"img\"] = [pytorch_transforms(img) for img in batch[\"img\"]]\n", + " return batch\n", + "\n", + " partition_train_test = partition_train_test.with_transform(apply_transforms)\n", + " trainloader = DataLoader(partition_train_test[\"train\"], batch_size=32, shuffle=True)\n", + " valloader = DataLoader(partition_train_test[\"test\"], batch_size=32)\n", + " testset = fds.load_split(\"test\").with_transform(apply_transforms)\n", + " testloader = DataLoader(testset, batch_size=32)\n", + " return trainloader, valloader, testloader" ] }, { @@ -182,7 +178,8 @@ " net.train()\n", " for epoch in range(epochs):\n", " correct, total, epoch_loss = 0, 0, 0.0\n", - " for images, labels in trainloader:\n", + " for batch in trainloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " optimizer.zero_grad()\n", " outputs = net(images)\n", @@ -204,7 +201,8 @@ " correct, total, loss = 0, 0, 0.0\n", " net.eval()\n", " with torch.no_grad():\n", - " for images, labels in testloader:\n", + " for batch in testloader:\n", + " images, labels = batch[\"img\"], batch[\"label\"]\n", " images, labels = images.to(DEVICE), labels.to(DEVICE)\n", " outputs = net(images)\n", " loss += criterion(outputs, labels).item()\n", @@ -222,7 +220,7 @@ "source": [ "## Step 1: Revisiting NumPyClient\n", "\n", - "So far, we've implemented our client by subclassing `flwr.client.NumPyClient`. The three methods we implemented are `get_parameters`, `fit`, and `evaluate`. Finally, we wrap the creation of instances of this class in a function called `client_fn`:" + "So far, we've implemented our client by subclassing `flwr.client.NumPyClient`. The three methods we implemented are `get_parameters`, `fit`, and `evaluate`. " ] }, { @@ -231,42 +229,60 @@ "metadata": {}, "outputs": [], "source": [ - "class FlowerNumPyClient(fl.client.NumPyClient):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerNumPyClient(NumPyClient):\n", + " def __init__(self, partition_id, net, trainloader, valloader):\n", + " self.partition_id = partition_id\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, config):\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.partition_id}] get_parameters\")\n", " return get_parameters(self.net)\n", "\n", " def fit(self, parameters, config):\n", - " print(f\"[Client {self.cid}] fit, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] fit, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " train(self.net, self.trainloader, epochs=1)\n", " return get_parameters(self.net), len(self.trainloader), {}\n", "\n", " def evaluate(self, parameters, config):\n", - " print(f\"[Client {self.cid}] evaluate, config: {config}\")\n", + " print(f\"[Client {self.partition_id}] evaluate, config: {config}\")\n", " set_parameters(self.net, parameters)\n", " loss, accuracy = test(self.net, self.valloader)\n", - " return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}\n", + " return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, we define the function `numpyclient_fn` that is used by Flower to create the `FlowerNumpyClient` instances on demand. Finally, we create the `ClientApp` and pass the `numpyclient_fn` to it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def numpyclient_fn(context: Context) -> Client:\n", + " net = Net().to(DEVICE)\n", + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerNumPyClient(partition_id, net, trainloader, valloader).to_client()\n", "\n", "\n", - "def numpyclient_fn(cid) -> FlowerNumPyClient:\n", - " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerNumPyClient(cid, net, trainloader, valloader)" + "# Create the ClientApp\n", + "numpyclient = ClientApp(client_fn=numpyclient_fn)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We've seen this before, there's nothing new so far. The only *tiny* difference compared to the previous notebook is naming, we've changed `FlowerClient` to `FlowerNumPyClient` and `client_fn` to `numpyclient_fn`. Let's run it to see the output we get:" + "We've seen this before, there's nothing new so far. The only *tiny* difference compared to the previous notebook is naming, we've changed `FlowerClient` to `FlowerNumPyClient` and `client_fn` to `numpyclient_fn`. Next, we configure the number of federated learning rounds using `ServerConfig` and create the `ServerApp` with this config:" ] }, { @@ -275,16 +291,43 @@ "metadata": {}, "outputs": [], "source": [ - "# Specify client resources if you need GPU (defaults to 1 CPU and 0 GPU)\n", - "client_resources = None\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Configure the server for 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(config=config)\n", + "\n", + "\n", + "# Create ServerApp\n", + "server = ServerApp(server_fn=server_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we specify the resources for each client and run the simulation to see the output we get:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Specify the resources each of your clients need\n", + "# If set to none, by default, each client will be allocated 2x CPU and 0x GPUs\n", + "backend_config = {\"client_resources\": None}\n", "if DEVICE.type == \"cuda\":\n", - " client_resources = {\"num_gpus\": 1}\n", + " backend_config = {\"client_resources\": {\"num_gpus\": 1}}\n", + "\n", + "NUM_PARTITIONS = 10\n", "\n", - "fl.simulation.start_simulation(\n", - " client_fn=numpyclient_fn,\n", - " num_clients=2,\n", - " config=fl.server.ServerConfig(num_rounds=3),\n", - " client_resources=client_resources,\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=numpyclient,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -292,9 +335,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This works as expected, two clients are training for three rounds of federated learning.\n", + "This works as expected, ten clients are training for three rounds of federated learning.\n", "\n", - "Let's dive a little bit deeper and discuss how Flower executes this simulation. Whenever a client is selected to do some work, `start_simulation` calls the function `numpyclient_fn` to create an instance of our `FlowerNumPyClient` (along with loading the model and the data).\n", + "Let's dive a little bit deeper and discuss how Flower executes this simulation. Whenever a client is selected to do some work, `run_simulation` launches the `ClientApp` object which in turn calls the function `numpyclient_fn` to create an instance of our `FlowerNumPyClient` (along with loading the model and the data).\n", "\n", "But here's the perhaps surprising part: Flower doesn't actually use the `FlowerNumPyClient` object directly. Instead, it wraps the object to makes it look like a subclass of `flwr.client.Client`, not `flwr.client.NumPyClient`. In fact, the Flower core framework doesn't know how to handle `NumPyClient`'s, it only knows how to handle `Client`'s. `NumPyClient` is just a convenience abstraction built on top of `Client`. \n", "\n", @@ -330,15 +373,15 @@ ")\n", "\n", "\n", - "class FlowerClient(fl.client.Client):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerClient(Client):\n", + " def __init__(self, partition_id, net, trainloader, valloader):\n", + " self.partition_id = partition_id\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, ins: GetParametersIns) -> GetParametersRes:\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.partition_id}] get_parameters\")\n", "\n", " # Get parameters as a list of NumPy ndarray's\n", " ndarrays: List[np.ndarray] = get_parameters(self.net)\n", @@ -354,7 +397,7 @@ " )\n", "\n", " def fit(self, ins: FitIns) -> FitRes:\n", - " print(f\"[Client {self.cid}] fit, config: {ins.config}\")\n", + " print(f\"[Client {self.partition_id}] fit, config: {ins.config}\")\n", "\n", " # Deserialize parameters to NumPy ndarray's\n", " parameters_original = ins.parameters\n", @@ -378,7 +421,7 @@ " )\n", "\n", " def evaluate(self, ins: EvaluateIns) -> EvaluateRes:\n", - " print(f\"[Client {self.cid}] evaluate, config: {ins.config}\")\n", + " print(f\"[Client {self.partition_id}] evaluate, config: {ins.config}\")\n", "\n", " # Deserialize parameters to NumPy ndarray's\n", " parameters_original = ins.parameters\n", @@ -386,7 +429,6 @@ "\n", " set_parameters(self.net, ndarrays_original)\n", " loss, accuracy = test(self.net, self.valloader)\n", - " # return float(loss), len(self.valloader), {\"accuracy\": float(accuracy)}\n", "\n", " # Build and return response\n", " status = Status(code=Code.OK, message=\"Success\")\n", @@ -398,11 +440,16 @@ " )\n", "\n", "\n", - "def client_fn(cid) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerClient(cid, net, trainloader, valloader)" + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerClient(partition_id, net, trainloader, valloader).to_client()\n", + "\n", + "\n", + "# Create the ClientApp\n", + "client = ClientApp(client_fn=client_fn)" ] }, { @@ -418,11 +465,12 @@ "metadata": {}, "outputs": [], "source": [ - "fl.simulation.start_simulation(\n", - " client_fn=client_fn,\n", - " num_clients=2,\n", - " config=fl.server.ServerConfig(num_rounds=3),\n", - " client_resources=client_resources,\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -578,15 +626,15 @@ ")\n", "\n", "\n", - "class FlowerClient(fl.client.Client):\n", - " def __init__(self, cid, net, trainloader, valloader):\n", - " self.cid = cid\n", + "class FlowerClient(Client):\n", + " def __init__(self, partition_id, net, trainloader, valloader):\n", + " self.partition_id = partition_id\n", " self.net = net\n", " self.trainloader = trainloader\n", " self.valloader = valloader\n", "\n", " def get_parameters(self, ins: GetParametersIns) -> GetParametersRes:\n", - " print(f\"[Client {self.cid}] get_parameters\")\n", + " print(f\"[Client {self.partition_id}] get_parameters\")\n", "\n", " # Get parameters as a list of NumPy ndarray's\n", " ndarrays: List[np.ndarray] = get_parameters(self.net)\n", @@ -602,7 +650,7 @@ " )\n", "\n", " def fit(self, ins: FitIns) -> FitRes:\n", - " print(f\"[Client {self.cid}] fit, config: {ins.config}\")\n", + " print(f\"[Client {self.partition_id}] fit, config: {ins.config}\")\n", "\n", " # Deserialize parameters to NumPy ndarray's using our custom function\n", " parameters_original = ins.parameters\n", @@ -626,7 +674,7 @@ " )\n", "\n", " def evaluate(self, ins: EvaluateIns) -> EvaluateRes:\n", - " print(f\"[Client {self.cid}] evaluate, config: {ins.config}\")\n", + " print(f\"[Client {self.partition_id}] evaluate, config: {ins.config}\")\n", "\n", " # Deserialize parameters to NumPy ndarray's using our custom function\n", " parameters_original = ins.parameters\n", @@ -645,11 +693,12 @@ " )\n", "\n", "\n", - "def client_fn(cid) -> FlowerClient:\n", + "def client_fn(context: Context) -> Client:\n", " net = Net().to(DEVICE)\n", - " trainloader = trainloaders[int(cid)]\n", - " valloader = valloaders[int(cid)]\n", - " return FlowerClient(cid, net, trainloader, valloader)" + " partition_id = context.node_config[\"partition-id\"]\n", + " num_partitions = context.node_config[\"num-partitions\"]\n", + " trainloader, valloader, _ = load_datasets(partition_id, num_partitions)\n", + " return FlowerClient(partition_id, net, trainloader, valloader).to_client()" ] }, { @@ -843,14 +892,24 @@ "metadata": {}, "outputs": [], "source": [ - "strategy = FedSparse()\n", + "def server_fn(context: Context) -> ServerAppComponents:\n", + " # Configure the server for just 3 rounds of training\n", + " config = ServerConfig(num_rounds=3)\n", + " return ServerAppComponents(\n", + " config=config,\n", + " strategy=FedSparse(), # <-- pass the new strategy here\n", + " )\n", + "\n", + "\n", + "# Create the ServerApp\n", + "server = ServerApp(server_fn=server_fn)\n", "\n", - "fl.simulation.start_simulation(\n", - " strategy=strategy,\n", - " client_fn=client_fn,\n", - " num_clients=2,\n", - " config=fl.server.ServerConfig(num_rounds=3),\n", - " client_resources=client_resources,\n", + "# Run simulation\n", + "run_simulation(\n", + " server_app=server,\n", + " client_app=client,\n", + " num_supernodes=NUM_PARTITIONS,\n", + " backend_config=backend_config,\n", ")" ] }, @@ -869,16 +928,16 @@ "source": [ "## Next steps\n", "\n", - "Before you continue, make sure to join the Flower community on Slack: [Join Slack](https://flower.ai/join-slack/)\n", + "Before you continue, make sure to join the Flower community on Flower Discuss ([Join Flower Discuss](https://discuss.flower.ai)) and on Slack ([Join Slack](https://flower.ai/join-slack/)).\n", "\n", "There's a dedicated `#questions` channel if you need help, but we'd also love to hear who you are in `#introductions`!\n", "\n", "This is the final part of the Flower tutorial (for now!), congratulations! You're now well equipped to understand the rest of the documentation. There are many topics we didn't cover in the tutorial, we recommend the following resources:\n", "\n", "- [Read Flower Docs](https://flower.ai/docs/)\n", - "- [Check out Flower Code Examples](https://github.com/adap/flower/tree/main/examples)\n", + "- [Check out Flower Code Examples](https://flower.ai/docs/examples/)\n", "- [Use Flower Baselines for your research](https://flower.ai/docs/baselines/)\n", - "- [Watch Flower Summit 2023 videos](https://flower.ai/conf/flower-summit-2023/)\n" + "- [Watch Flower AI Summit 2024 videos](https://flower.ai/conf/flower-ai-summit-2024/)\n" ] } ], From 28b5505a93f263bb5ebaa4c9dd2246d3eb67e3eb Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 26 Jul 2024 18:50:05 +0200 Subject: [PATCH 015/188] fix(framework:skip) Replace deprecated function in JAX template (#3930) --- src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl index 82f080ebcdcb..72fe440c978a 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl @@ -33,7 +33,7 @@ def train(params, grad_fn, X, y): num_examples = X.shape[0] for epochs in range(50): grads = grad_fn(params, X, y) - params = jax.tree.map(lambda p, g: p - 0.05 * g, params, grads) + params = jax.tree_map(lambda p, g: p - 0.05 * g, params, grads) loss = loss_fn(params, X, y) return params, loss, num_examples From ad811b5a0afc8bd32fb27305a8d0063f41a09ce5 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 26 Jul 2024 23:01:21 +0200 Subject: [PATCH 016/188] ci(framework:skip) Make sure E2E template tests fail on ERROR (#3931) --- .github/workflows/e2e.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f5ed1d99012a..4ad54fae8fa9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -213,7 +213,7 @@ jobs: needs: wheel strategy: matrix: - framework: ["numpy", "pytorch", "tensorflow", "huggingface", "jax", "sklearn"] + framework: ["numpy", "pytorch", "tensorflow", "jax", "sklearn"] name: Template / ${{ matrix.framework }} @@ -234,12 +234,10 @@ jobs: flwr new tmp-${{ matrix.framework }} --framework ${{ matrix.framework }} --username gh_ci cd tmp-${{ matrix.framework }} pip install . - - name: Cache Datasets - uses: actions/cache@v4 - with: - path: "~/.cache/huggingface/datasets" - key: ${{ matrix.framework }}-template-datasets - name: Run project run: | cd tmp-${{ matrix.framework }} - flwr run --run-config num-server-rounds=1 + flwr run --run-config num-server-rounds=1 2>&1 | tee flwr_output.log + if grep -q "ERROR" flwr_output.log; then + exit 1 + fi From 2964a8e13243a30f286115528204ecccb10d3d2a Mon Sep 17 00:00:00 2001 From: Edoardo Gabrielli Date: Tue, 30 Jul 2024 09:12:52 +0200 Subject: [PATCH 017/188] feat(baselines) Add `Flanders` baseline (#2620) Co-authored-by: jafermarq --- baselines/flanders/.gitignore | 9 + baselines/flanders/LICENSE | 202 +++++++ baselines/flanders/README.md | 157 ++++++ baselines/flanders/_static/screenshot-1.png | Bin 0 -> 59693 bytes baselines/flanders/_static/screenshot-2.png | Bin 0 -> 55468 bytes baselines/flanders/_static/screenshot-3.png | Bin 0 -> 54775 bytes baselines/flanders/_static/screenshot-4.png | Bin 0 -> 13737 bytes baselines/flanders/_static/screenshot-5.png | Bin 0 -> 12330 bytes baselines/flanders/_static/screenshot-6.png | Bin 0 -> 12285 bytes baselines/flanders/_static/screenshot-8.png | Bin 0 -> 65193 bytes baselines/flanders/_static/screenshot.png | Bin 0 -> 33483 bytes baselines/flanders/flanders/__init__.py | 1 + baselines/flanders/flanders/attacks.py | 493 ++++++++++++++++++ baselines/flanders/flanders/client.py | 174 +++++++ .../flanders/conf/aggregate_fn/bulyan.yaml | 9 + .../flanders/conf/aggregate_fn/fedavg.yaml | 6 + .../flanders/conf/aggregate_fn/fedmedian.yaml | 6 + .../flanders/conf/aggregate_fn/krum.yaml | 7 + .../conf/aggregate_fn/trimmedmean.yaml | 7 + baselines/flanders/flanders/conf/base.yaml | 27 + .../flanders/conf/strategy/bulyan.yaml | 8 + .../flanders/conf/strategy/fedavg.yaml | 5 + .../flanders/conf/strategy/fedmedian.yaml | 5 + .../flanders/conf/strategy/flanders.yaml | 10 + .../flanders/flanders/conf/strategy/krum.yaml | 7 + .../flanders/conf/strategy/trimmedmean.yaml | 6 + baselines/flanders/flanders/dataset.py | 289 ++++++++++ .../flanders/flanders/dataset_preparation.py | 490 +++++++++++++++++ baselines/flanders/flanders/main.py | 279 ++++++++++ baselines/flanders/flanders/models.py | 164 ++++++ baselines/flanders/flanders/server.py | 384 ++++++++++++++ baselines/flanders/flanders/strategy.py | 375 +++++++++++++ baselines/flanders/flanders/utils.py | 182 +++++++ .../flanders/plotting/FLANDERS_results.ipynb | 1 + baselines/flanders/pyproject.toml | 151 ++++++ baselines/flanders/run.sh | 9 + 36 files changed, 3463 insertions(+) create mode 100644 baselines/flanders/.gitignore create mode 100644 baselines/flanders/LICENSE create mode 100644 baselines/flanders/README.md create mode 100644 baselines/flanders/_static/screenshot-1.png create mode 100644 baselines/flanders/_static/screenshot-2.png create mode 100644 baselines/flanders/_static/screenshot-3.png create mode 100644 baselines/flanders/_static/screenshot-4.png create mode 100644 baselines/flanders/_static/screenshot-5.png create mode 100644 baselines/flanders/_static/screenshot-6.png create mode 100644 baselines/flanders/_static/screenshot-8.png create mode 100644 baselines/flanders/_static/screenshot.png create mode 100644 baselines/flanders/flanders/__init__.py create mode 100644 baselines/flanders/flanders/attacks.py create mode 100644 baselines/flanders/flanders/client.py create mode 100644 baselines/flanders/flanders/conf/aggregate_fn/bulyan.yaml create mode 100644 baselines/flanders/flanders/conf/aggregate_fn/fedavg.yaml create mode 100644 baselines/flanders/flanders/conf/aggregate_fn/fedmedian.yaml create mode 100644 baselines/flanders/flanders/conf/aggregate_fn/krum.yaml create mode 100644 baselines/flanders/flanders/conf/aggregate_fn/trimmedmean.yaml create mode 100644 baselines/flanders/flanders/conf/base.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/bulyan.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/fedavg.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/fedmedian.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/flanders.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/krum.yaml create mode 100644 baselines/flanders/flanders/conf/strategy/trimmedmean.yaml create mode 100644 baselines/flanders/flanders/dataset.py create mode 100644 baselines/flanders/flanders/dataset_preparation.py create mode 100644 baselines/flanders/flanders/main.py create mode 100644 baselines/flanders/flanders/models.py create mode 100644 baselines/flanders/flanders/server.py create mode 100644 baselines/flanders/flanders/strategy.py create mode 100644 baselines/flanders/flanders/utils.py create mode 100644 baselines/flanders/plotting/FLANDERS_results.ipynb create mode 100644 baselines/flanders/pyproject.toml create mode 100644 baselines/flanders/run.sh diff --git a/baselines/flanders/.gitignore b/baselines/flanders/.gitignore new file mode 100644 index 000000000000..4187d73689f0 --- /dev/null +++ b/baselines/flanders/.gitignore @@ -0,0 +1,9 @@ +outputs/* +clients_params/* +flanders/datasets_files/* +*.log +flanders/__pycache__ +MNIST +.DS_Store +*/__pycache__ +multirun \ No newline at end of file diff --git a/baselines/flanders/LICENSE b/baselines/flanders/LICENSE new file mode 100644 index 000000000000..d64569567334 --- /dev/null +++ b/baselines/flanders/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/baselines/flanders/README.md b/baselines/flanders/README.md new file mode 100644 index 000000000000..f5ab6a02d6f3 --- /dev/null +++ b/baselines/flanders/README.md @@ -0,0 +1,157 @@ +--- +title: Protecting Federated Learning from Extreme Model Poisoning Attacks via Multidimensional Time Series Anomaly Detection +url: https://arxiv.org/abs/2303.16668 +labels: [robustness, model poisoning, anomaly detection, autoregressive model, regression, classification] +dataset: [MNIST, FashionMNIST] +--- + +**Paper:** [arxiv.org/abs/2303.16668](https://arxiv.org/abs/2303.16668) + +**Authors:** Edoardo Gabrielli, Gabriele Tolomei, Dimitri Belli, Vittorio Miori + +**Abstract:** Current defense mechanisms against model poisoning attacks in federated learning (FL) systems have proven effective up to a certain threshold of malicious clients. In this work, we introduce FLANDERS, a novel pre-aggregation filter for FL resilient to large-scale model poisoning attacks, i.e., when malicious clients far exceed legitimate participants. FLANDERS treats the sequence of local models sent by clients in each FL round as a matrix-valued time series. Then, it identifies malicious client updates as outliers in this time series by comparing actual observations with estimates generated by a matrix autoregressive forecasting model maintained by the server. Experiments conducted in several non-iid FL setups show that FLANDERS significantly improves robustness across a wide spectrum of attacks when paired with standard and robust existing aggregation methods. + +## About this baseline + +**What’s implemented:** The code in this directory replicates the results of FLANDERS+\[baseline\] on MNIST and Fashion-MNIST under all attack settings: Gaussian, LIE, OPT, and AGR-MM; with $r=[0.2,0.6,0.8]$ (i.e., the fraction of malicious clients), specifically about tables 1, 3, 10, 11, 15, 17, 19, 20 and Figure 3. + +**Datasets:** MNIST, FMNIST + +**Hardware Setup:** AMD Ryzen 9, 64 GB RAM, and an NVIDIA 4090 GPU with 24 GB VRAM. + +**Estimated time to run:** You can expect to run experiments on the given setup in 2m with *MNIST* and 3m with *Fashion-MNIST*, without attacks. With an Apple M2 Pro, 16gb RAM, each experiment with 10 clients for MNIST runs in about 24 minutes. Note that experiments with OPT (fang) and AGR-MM (minmax) can be up to 5x times slower. + +**Contributors:** Edoardo Gabrielli, Sapienza University of Rome ([GitHub](https://github.com/edogab33), [Scholar](https://scholar.google.com/citations?user=b3bePdYAAAAJ)) + + +## Experimental Setup + +Please, checkout Appendix F and G of the paper for a comprehensive overview of the hyperparameters setup, however here's a summary. + +**Task:** Image classification + +**Models:** + +MNIST (multilabel classification, fully connected, feed forward NN): +- Multilevel Perceptron (MLP) +- minimizing multiclass cross-entropy loss using Adam optimizer +- input: 784 +- hidden layer 1: 128 +- hidden layer 2: 256 + +Fashion-MNIST (multilabel classification, fully connected, feed forward NN): +- Multilevel Perceptron (MLP) +- minimizing multiclass cross-entropy loss using Adam optimizer +- input: 784 +- hidden layer 1: 256 +- hidden layer 2: 128 +- hidden layer 3: 64 + +**Dataset:** Every dataset is partitioned into two disjoint sets: 80% for training and 20% for testing. The training set is distributed across all clients (100) by using the Dirichlet distribution with $\alpha=0.5$, simulating a high non-i.i.d. scenario, while the testing set is uniform and held by the server to evaluate the global model. + +| Description | Default Value | +| ----------- | ----- | +| Partitions | 100 | +| Evaluation | centralized | +| Training set | 80% | +| Testing set | 20% | +| Distribution | Dirichlet | +| $\alpha$ | 0.5 | + +**Training Hyperparameters:** + +| Dataset | # of clients | Clients per round | # of rounds | Batch size | Learning rate | Optimizer | Dropout | Alpha | Beta | # of clients to keep | Sampling | +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| MNIST | 100 | 100 | 50 | 32 | $10^{-3}$ | Adam | 0.2 | 0.0 | 0.0 | $m - b$ | 500 | +| FMNIST | 100 | 100 | 50 | 32 | $10^{-3}$ | Adam | 0.2 | 0.0 | 0.0 | $m - b$ | 500 | + +Where $m$ is the number of clients partecipating during n-th round and $b$ is the number of malicious clients. The variable $sampling$ identifies how many parameters MAR analyzes. + + +## Environment Setup + +```bash +# Use a version of Python >=3.9 and <3.12.0. +pyenv local 3.10.12 +poetry env use 3.10.12 + +# Install everything from the toml +poetry install + +# Activate the env +poetry shell +``` + + +## Running the Experiments +Ensure that the environment is properly set up, then run: + +```bash +python -m flanders.main +``` + +To execute a single experiment with the default values in `conf/base.yaml`. + +To run custom experiments, you can override the default values like that: + +```bash +python -m flanders.main dataset=mnist server.attack_fn=lie server.num_malicious=1 +``` + +To run multiple custom experiments: + +```bash +python -m flanders.main --multirun dataset=mnist,fmnist server.attack_fn=gaussian,lie,fang,minmax server.num_malicious=0,1,2,3,4,5 +``` + +## Expected Results + +To run all the experiments of the paper (for MNIST and Fashion-MNIST), I've set up a script: + +```bash +sh run.sh +``` + +This code will produce the output in the file `outputs/all_results.csv`. To generate the plots and tables displayed below, you can use the notebook in the `plotting/` directory. + + +### Accuracy over multiple rounds +**(left) MNIST, FLANDERS+FedAvg with 80% of malicious clients (b = 80); (right) Vanilla FedAvg in the same setting:** + +![acc_over_rounds](_static/screenshot-8.png) + +### Precision and Recall of FLANDERS + +**b = 20:** + +![alt text](_static/screenshot-4.png) +--- + +**b = 60:** + +![alt text](_static/screenshot-5.png) +--- +**b = 80:** + +![alt text](_static/screenshot-6.png) + + +### Accuracy w.r.t. number of attackers: +**b = 0:** + +![alt text](_static/screenshot.png) + +--- +**b = 20:** + +![alt text](_static/screenshot-1.png) + +--- +**b = 60:** + +![alt text](_static/screenshot-2.png) + +--- +**b = 80:** + +![alt text](_static/screenshot-3.png) diff --git a/baselines/flanders/_static/screenshot-1.png b/baselines/flanders/_static/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c14a7e72f27b08b7447315e272a6040ff5315c GIT binary patch literal 59693 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 E0Ei#YhX4Qo literal 0 HcmV?d00001 diff --git a/baselines/flanders/_static/screenshot-2.png b/baselines/flanders/_static/screenshot-2.png new file mode 100644 index 0000000000000000000000000000000000000000..7aacd2ba577838995458d3e032b50707418e20f4 GIT binary patch literal 55468 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�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

kW8Q0l~meg`d~18aRV9!#UIsDqO{Go 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

22U!;?Zu48P(_| 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 NNQujfm5Ue#{2#YARk8p8 literal 0 HcmV?d00001 diff --git a/baselines/flanders/_static/screenshot-5.png b/baselines/flanders/_static/screenshot-5.png new file mode 100644 index 0000000000000000000000000000000000000000..e0defab01d22801c8227ca211bf6840554a22cee GIT binary patch literal 12330 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 Bj<2T3U$ky);pMF9BtqS`IQ=>d1(s_@AMZP&gH5 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 literal 0 HcmV?d00001 diff --git a/baselines/flanders/_static/screenshot-8.png b/baselines/flanders/_static/screenshot-8.png new file mode 100644 index 0000000000000000000000000000000000000000..cda98c21d0349fbe7b02601839c002b934d50791 GIT binary patch literal 65193 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≫n* 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 literal 0 HcmV?d00001 diff --git a/baselines/flanders/flanders/__init__.py b/baselines/flanders/flanders/__init__.py new file mode 100644 index 000000000000..eb3edd489459 --- /dev/null +++ b/baselines/flanders/flanders/__init__.py @@ -0,0 +1 @@ +"""FLANDERS package.""" diff --git a/baselines/flanders/flanders/attacks.py b/baselines/flanders/flanders/attacks.py new file mode 100644 index 000000000000..9b1acd9ad639 --- /dev/null +++ b/baselines/flanders/flanders/attacks.py @@ -0,0 +1,493 @@ +"""Implementation of attacks used in the paper.""" + +import math +from typing import Dict, List, Tuple + +import numpy as np +from flwr.common import FitRes, ndarrays_to_parameters, parameters_to_ndarrays +from flwr.server.client_proxy import ClientProxy +from scipy.stats import norm + + +# pylint: disable=unused-argument +def no_attack( + ordered_results: List[Tuple[ClientProxy, FitRes]], states: Dict[str, bool], **kwargs +): + """No attack.""" + return ordered_results, {} + + +def gaussian_attack(ordered_results, states, **kwargs): + """Apply Gaussian attack on parameters. + + Parameters + ---------- + ordered_results + List of tuples (client_proxy, fit_result) ordered by client id. + states + Dictionary of client ids and their states (True if malicious, False otherwise). + magnitude + Magnitude of the attack. + dataset_name + Name of the dataset. + + Returns + ------- + results + List of tuples (client_proxy, fit_result) ordered by client id. + """ + magnitude = kwargs.get("magnitude", 0.0) + dataset_name = kwargs.get("dataset_name", "no name") + results = ordered_results.copy() + + def perturbate(vect): + return vect + np.random.normal(loc=0, scale=magnitude, size=vect.size) + + for proxy, fitres in ordered_results: + if states[fitres.metrics["cid"]]: + params = parameters_to_ndarrays(fitres.parameters) + if dataset_name == "income": + new_params = [perturbate(layer) for layer in params] + else: + new_params = [] + for par in params: + # if par is an array of one element, it is a scalar + if par.size == 1: + new_params.append(perturbate(par)) + else: + new_params.append(np.apply_along_axis(perturbate, 0, par)) + fitres.parameters = ndarrays_to_parameters(new_params) + results[int(fitres.metrics["cid"])] = (proxy, fitres) + return results, {} + + +# pylint: disable=too-many-locals, unused-argument +def lie_attack( + ordered_results, + states, + omniscent=True, + **kwargs, +): + """Apply Omniscent LIE attack, Baruch et al. (2019) on parameters. + + Parameters + ---------- + ordered_results + List of tuples (client_proxy, fit_result) ordered by client id. + states + Dictionary of client ids and their states (True if malicious, False otherwise). + omniscent + Whether the attacker knows the local models of all clients or not. + + Returns + ------- + results + List of tuples (client_proxy, fit_result) ordered by client id. + """ + results = ordered_results.copy() + params = [parameters_to_ndarrays(fitres.parameters) for _, fitres in results] + grads_mean = [np.mean(layer, axis=0) for layer in zip(*params)] + grads_stdev = [np.std(layer, axis=0) ** 0.5 for layer in zip(*params)] + + if not omniscent: + # if not omniscent, the attacker doesn't know the + # local models of all clients, but only of the corrupted ones + params = [ + params[i] + for i in range(len(params)) + if states[results[i][1].metrics["cid"]] + ] + + num_clients = len(ordered_results) + num_malicious = sum(val is True for val in states.values()) + + # pylint: disable=c-extension-no-member + num_supporters = math.floor((num_clients / 2) + 1) - num_malicious + + z_max = norm.cdf( + (num_clients - num_malicious - num_supporters) / (num_clients - num_malicious) + ) + + for proxy, fitres in ordered_results: + if states[fitres.metrics["cid"]]: + mul_std = [layer * z_max for layer in grads_stdev] + new_params = [grads_mean[i] - mul_std[i] for i in range(len(grads_mean))] + fitres.parameters = ndarrays_to_parameters(new_params) + results[int(fitres.metrics["cid"])] = (proxy, fitres) + return results, {} + + +def fang_attack( + ordered_results, + states, + omniscent=True, + **kwargs, +): + """Apply Local Model Poisoning Attacks. + + (Fang et al. (2020)) + Specifically designed for Krum, but they claim it works for other + aggregation functions as well. + Full-knowledge version (attackers knows the local models of all clients). + + Parameters + ---------- + ordered_results + List of tuples (client_proxy, fit_result) ordered by client id. + states + Dictionary of client ids and their states (True if malicious, False + otherwise). + omniscent + Whether the attacker knows the local models of all clients or not. + num_layers + Number of layers. + w_re + The received global model. + old_lambda + The lambda from the previous round. + threshold + The threshold for lambda. + malicious_selected + Whether the attacker was selected as malicious in the previous round. + + Returns + ------- + results + List of tuples (client_proxy, fit_result) ordered by client id. + """ + num_layers = kwargs.get("num_layers", 2) + w_re = kwargs.get("w_re", None) # the received global model + threshold = kwargs.get("threshold", 1e-5) + + num_clients = len(ordered_results) + num_corrupted = sum(val is True for val in states.values()) + # there can't be an attack with less than 2 malicious clients + # to avoid division by 0 + num_corrupted = max(num_corrupted, 2) + + if not omniscent: + # if not omniscent, the attacker doesn't know the + # local models of all clients, but only of the corrupted ones + ordered_results = [ + ordered_results[i] + for i in range(len(ordered_results)) + if states[ordered_results[i][1].metrics["cid"]] + ] + + # Initialize lambda + benign = [ + (parameters_to_ndarrays(fitres.parameters), fitres.num_examples) + for _, fitres in ordered_results + if states[fitres.metrics["cid"]] is False + ] + all_params = [ + (parameters_to_ndarrays(fitres.parameters), fitres.num_examples) + for _, fitres in ordered_results + ] + # Compute the smallest distance that Krum would choose + _, _, _, distances = _krum(all_params, num_corrupted, 1) + + idx_benign = [int(cid) for cid in states.keys() if states[cid] is False] + + min_dist = np.min(np.array(distances)[idx_benign]) / ( + ((num_clients - 2) * (num_corrupted - 1)) * np.sqrt(num_layers) + ) + + # Compute max distance from w_re + dist_wre = np.zeros((len(benign))) + for i in range(len(benign)): + dist = [benign[i][0][j] - w_re[j] for j in range(num_layers)] + norm_sums = 0 + for k in dist: + norm_sums += np.linalg.norm(k) + dist_wre[i] = norm_sums**2 + max_dist = np.max(dist_wre) / np.sqrt(num_layers) + lamda = min( + min_dist + max_dist, 999 + ) # lambda (capped to 999 to avoid numerical problems in specific settings) + + malicious_selected, corrupted_params = _fang_corrupt_and_select( + all_params, w_re, states, num_corrupted, lamda + ) + while lamda > threshold and malicious_selected is False: + lamda = lamda * 0.5 + malicious_selected, corrupted_params = _fang_corrupt_and_select( + all_params, w_re, states, num_corrupted, lamda + ) + + # Set corrupted clients' updates to w_1 + results = [ + ( + ( + proxy, + FitRes( + fitres.status, + parameters=ndarrays_to_parameters(corrupted_params), + num_examples=fitres.num_examples, + metrics=fitres.metrics, + ), + ) + if states[fitres.metrics["cid"]] + else (proxy, fitres) + ) + for proxy, fitres in ordered_results + ] + + return results, {} + + +def minmax_attack( + ordered_results, + states, + omniscent=True, + **kwargs, +): + """Apply Min-Max agnostic attack. + + Full-knowledge, perturbation function chosen according to our experimental + results. + From: + "Manipulating the Byzantine: Optimizing Model Poisoning Attacks and + Defenses for Federated Learning" (Shejwalkar et al., 2021) + + Parameters + ---------- + ordered_results + List of tuples (client_proxy, fit_result) ordered by client id. + states + Dictionary of client ids and their states (True if malicious, False + otherwise). + omniscent + Whether the attacker knows the local models of all clients or not. + threshold + Threshold for lambda. + lambda_init + Initial value for lambda. + + Returns + ------- + results + List of tuples (client_proxy, fit_result) ordered by client id. + """ + dataset_name = kwargs.get("dataset_name", None) + threshold = kwargs.get("threshold", 1e-5) + lambda_init = kwargs.get("lambda", 5.0) + malicious_num = kwargs.get("malicious_num", 0) + + results = ordered_results.copy() + params = [parameters_to_ndarrays(fitres.parameters) for _, fitres in results] + params_avg = [np.mean(param, axis=0) for param in zip(*params)] + + if not omniscent: + # if not omniscent, the attacker doesn't know the + # local models of all clients, but only of the corrupted ones + results = [ + results[i] + for i in range(len(results)) + if states[results[i][1].metrics["cid"]] + ] + + # Decide what perturbation to use according to the + # results presented in the paper. + if dataset_name == "mnist": + # Apply std perturbation + # In the paper authors state that sign function is the best + # but in my experience std perturbation works better + perturbation_vect = [-np.std(layer, axis=0) for layer in zip(*params)] + elif dataset_name == "cifar": + # Apply std perturbation + perturbation_vect = [-np.std(layer, axis=0) for layer in zip(*params)] + else: + # Apply std perturbation + perturbation_vect = [-np.std(layer, axis=0) for layer in zip(*params)] + + # Compute lambda (referred as gamma in the paper) + lambda_succ = lambda_init + 1 + curr_lambda = lambda_init + step = lambda_init * 0.5 + while ( + abs(lambda_succ - curr_lambda) > threshold + and step > threshold + and malicious_num > 0 + ): + # Compute malicious gradients + perturbed_params = [ + curr_lambda * perturbation_vect[i] for i in range(len(perturbation_vect)) + ] + corrupted_params = [ + params_avg[i] + perturbed_params[i] for i in range(len(params_avg)) + ] + + # Set corrupted clients' updates to corrupted_params + params_c = [ + corrupted_params if states[str(i)] else params[i] + for i in range(len(params)) + ] + distance_matrix = _compute_distances(params_c) + + # Remove from matrix distance_matrix all malicious clients in both + # rows and columns + distance_matrix_b = np.delete( + distance_matrix, + [ + i + for i in range(len(distance_matrix)) + if states[results[i][1].metrics["cid"]] + ], + axis=0, + ) + distance_matrix_b = np.delete( + distance_matrix_b, + [ + i + for i in range(len(distance_matrix)) + if states[results[i][1].metrics["cid"]] + ], + axis=1, + ) + + # Remove from distance_matrix all benign clients on + # rows and all malicious on columns + distance_matrix_m = np.delete( + distance_matrix, + [ + i + for i in range(len(distance_matrix)) + if not states[results[i][1].metrics["cid"]] + ], + axis=0, + ) + distance_matrix_m = np.delete( + distance_matrix_m, + [ + i + for i in range(len(distance_matrix)) + if states[results[i][1].metrics["cid"]] + ], + axis=1, + ) + + # Take the maximum distance between any benign client and any malicious one + max_dist_m = np.max(distance_matrix_m) + + # Take the maximum distance between any two benign clients + max_dist_b = np.max(distance_matrix_b) + + # Compute lambda (best scaling coefficient) + if max_dist_m < max_dist_b: + # Lambda (gamma in the paper) is good. Save and try to increase it + lambda_succ = curr_lambda + curr_lambda = curr_lambda + step * 0.5 + else: + # Lambda is to big, must be reduced to increse the chances of being selected + curr_lambda = curr_lambda - step * 0.5 + step *= 0.5 + + # Compute the final malicious update + perturbation_vect = [ + lambda_succ * perturbation_vect[i] for i in range(len(perturbation_vect)) + ] + corrupted_params = [ + params_avg[i] + perturbation_vect[i] for i in range(len(params_avg)) + ] + corrupted_params = ndarrays_to_parameters(corrupted_params) + for proxy, fitres in ordered_results: + if states[fitres.metrics["cid"]]: + fitres.parameters = corrupted_params + results[int(fitres.metrics["cid"])] = (proxy, fitres) + return results, {} + + +def _krum(results, num_malicious, to_keep, num_closest=None): + """Get the best parameters vector according to the Krum function. + + Output: the best parameters vector. + """ + weights = [w for w, _ in results] # list of weights + distance_matrix = _compute_distances(weights) # matrix of distances + + if not num_closest: + num_closest = ( + len(weights) - num_malicious - 2 + ) # number of closest points to use + if num_closest <= 0: + num_closest = 1 + elif num_closest > len(weights): + num_closest = len(weights) + + closest_indices = _get_closest_indices( + distance_matrix, num_closest + ) # indices of closest points + + scores = [ + np.sum(distance_matrix[i, closest_indices[i]]) + for i in range(len(distance_matrix)) + ] # scores i->j for each i + + best_index = np.argmin(scores) # index of the best score + best_indices = np.argsort(scores)[::-1][ + len(scores) - to_keep : + ] # indices of best scores (multikrum) + return weights[best_index], best_index, best_indices, scores + + +def _compute_distances(weights): + """Compute distances between vectors. + + Input: weights - list of weights vectors + Output: distances - matrix distance_matrix of squared distances between the vectors + """ + flat_w = np.array([np.concatenate(par, axis=None).ravel() for par in weights]) + distance_matrix = np.zeros((len(weights), len(weights))) + for i, _ in enumerate(flat_w): + for j, _ in enumerate(flat_w): + delta = flat_w[i] - flat_w[j] + dist = np.linalg.norm(delta) + distance_matrix[i, j] = dist**2 + return distance_matrix + + +def _get_closest_indices(distance_matrix, num_closest): + """Get the indices of the closest points. + + Args: + distance_matrix + matrix of distances + num_closest + number of closest points to get for each parameter vector + Output: + closest_indices + list of lists of indices of the closest points for each vector. + """ + closest_indices = [] + for idx, _ in enumerate(distance_matrix): + closest_indices.append( + np.argsort(distance_matrix[idx])[1 : num_closest + 1].tolist() + ) + return closest_indices + + +def _fang_corrupt_params(global_model, lamda): + # Compute sign vector num_supporters + magnitude = [] + for i, _ in enumerate(global_model): + magnitude.append(np.sign(global_model[i]) * lamda) + + corrupted_params = [ + global_model[i] - magnitude[i] for i in range(len(global_model)) + ] # corrupted model + return corrupted_params + + +def _fang_corrupt_and_select(all_models, global_model, states, num_corrupted, lamda): + # Check that krum selects a malicious client + corrupted_params = _fang_corrupt_params(global_model, lamda) + all_models_m = [ + (corrupted_params, num_examples) if states[str(i)] else (model, num_examples) + for i, (model, num_examples) in enumerate(all_models) + ] + _, idx_best_model, _, _ = _krum(all_models_m, num_corrupted, 1) + + # Check if the best model is malicious + malicious_selected = states[str(idx_best_model)] + return malicious_selected, corrupted_params diff --git a/baselines/flanders/flanders/client.py b/baselines/flanders/flanders/client.py new file mode 100644 index 000000000000..57513ccf7291 --- /dev/null +++ b/baselines/flanders/flanders/client.py @@ -0,0 +1,174 @@ +"""Clients implementation for Flanders.""" + +from collections import OrderedDict +from pathlib import Path +from typing import Tuple + +import flwr as fl +import numpy as np +import ray +import torch + +from .dataset import get_dataloader, mnist_transformation +from .models import ( + FMnistNet, + MnistNet, + test_fmnist, + test_mnist, + train_fmnist, + train_mnist, +) + +XY = Tuple[np.ndarray, np.ndarray] + + +def get_params(model): + """Get model weights as a list of NumPy ndarrays.""" + return [val.cpu().numpy() for _, val in model.state_dict().items()] + + +def set_params(model, params): + """Set model weights from a list of NumPy ndarrays.""" + params_dict = zip(model.state_dict().keys(), params) + state_dict = OrderedDict({k: torch.from_numpy(np.copy(v)) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) + + +class MnistClient(fl.client.NumPyClient): + """Implementation of MNIST image classification using PyTorch.""" + + def __init__(self, cid, fed_dir_data): + """Instantiate a client for the MNIST dataset.""" + self.cid = cid + self.fed_dir = Path(fed_dir_data) + self.properties = {"tensor_type": "numpy.ndarray"} + + # Instantiate model + self.net = MnistNet() + + # Determine device + # self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if torch.cuda.is_available(): + self.device = torch.device("cuda") + elif torch.backends.mps.is_available(): + self.device = torch.device("mps") + else: + self.device = torch.device("cpu") + + def get_parameters(self, config): + """Get model parameters as a list of NumPy ndarrays.""" + return get_params(self.net) + + def fit(self, parameters, config): + """Set model parameters from a list of NumPy ndarrays.""" + set_params(self.net, parameters) + + # Load data for this client and get trainloader + num_workers = 1 + trainloader = get_dataloader( + self.fed_dir, + self.cid, + is_train=True, + batch_size=config["batch_size"], + workers=num_workers, + transform=mnist_transformation, + ) + + self.net.to(self.device) + train_mnist(self.net, trainloader, epochs=config["epochs"], device=self.device) + + return ( + get_params(self.net), + len(trainloader.dataset), + {"cid": self.cid, "malicious": config["malicious"]}, + ) + + def evaluate(self, parameters, config): + """Evaluate using local test dataset.""" + set_params(self.net, parameters) + + # Load data for this client and get trainloader + num_workers = len(ray.worker.get_resource_ids()["CPU"]) + valloader = get_dataloader( + self.fed_dir, + self.cid, + is_train=False, + batch_size=50, + workers=num_workers, + transform=mnist_transformation, + ) + + self.net.to(self.device) + loss, accuracy = test_mnist(self.net, valloader, device=self.device) + + return float(loss), len(valloader.dataset), {"accuracy": float(accuracy)} + + +class FMnistClient(fl.client.NumPyClient): + """Implementation of MNIST image classification using PyTorch.""" + + def __init__(self, cid, fed_dir_data): + """Instantiate a client for the MNIST dataset.""" + self.cid = cid + self.fed_dir = Path(fed_dir_data) + self.properties = {"tensor_type": "numpy.ndarray"} + + # Instantiate model + self.net = FMnistNet() + + # Determine device + # self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if torch.cuda.is_available(): + self.device = torch.device("cuda") + elif torch.backends.mps.is_available(): + self.device = torch.device("mps") + else: + self.device = torch.device("cpu") + + def get_parameters(self, config): + """Get model parameters as a list of NumPy ndarrays.""" + return get_params(self.net) + + def fit(self, parameters, config): + """Set model parameters from a list of NumPy ndarrays.""" + set_params(self.net, parameters) + + # Load data for this client and get trainloader + num_workers = 1 + trainloader = get_dataloader( + self.fed_dir, + self.cid, + is_train=True, + batch_size=config["batch_size"], + workers=num_workers, + transform=mnist_transformation, + ) + + self.net.to(self.device) + train_fmnist(self.net, trainloader, epochs=config["epochs"], device=self.device) + + return ( + get_params(self.net), + len(trainloader.dataset), + {"cid": self.cid, "malicious": config["malicious"]}, + ) + + def evaluate(self, parameters, config): + """Evaluate using local test dataset.""" + set_params(self.net, parameters) + + # Load data for this client and get trainloader + num_workers = len(ray.worker.get_resource_ids()["CPU"]) + valloader = get_dataloader( + self.fed_dir, + self.cid, + is_train=False, + batch_size=50, + workers=num_workers, + transform=mnist_transformation, + ) + + self.net.to(self.device) + loss, accuracy = test_fmnist(self.net, valloader, device=self.device) + + return float(loss), len(valloader.dataset), {"accuracy": float(accuracy)} diff --git a/baselines/flanders/flanders/conf/aggregate_fn/bulyan.yaml b/baselines/flanders/flanders/conf/aggregate_fn/bulyan.yaml new file mode 100644 index 000000000000..1361f158daf1 --- /dev/null +++ b/baselines/flanders/flanders/conf/aggregate_fn/bulyan.yaml @@ -0,0 +1,9 @@ +--- +name: bulyan + +aggregate_fn: + function: flwr.server.strategy.aggregate.aggregate_bulyan + parameters: + aggregation_name: aggregate_krum + aggregation_module_name: flwr.server.strategy.aggregate + to_keep: 0 # if 0, normal Krum is applied \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/aggregate_fn/fedavg.yaml b/baselines/flanders/flanders/conf/aggregate_fn/fedavg.yaml new file mode 100644 index 000000000000..826a4163b2eb --- /dev/null +++ b/baselines/flanders/flanders/conf/aggregate_fn/fedavg.yaml @@ -0,0 +1,6 @@ +--- +name: fedavg + +aggregate_fn: + function: flwr.server.strategy.aggregate.aggregate + parameters: {} \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/aggregate_fn/fedmedian.yaml b/baselines/flanders/flanders/conf/aggregate_fn/fedmedian.yaml new file mode 100644 index 000000000000..7bf0a725ab6f --- /dev/null +++ b/baselines/flanders/flanders/conf/aggregate_fn/fedmedian.yaml @@ -0,0 +1,6 @@ +--- +name: fedmedian + +aggregate_fn: + function: flwr.server.strategy.aggregate.aggregate_median + parameters: {} \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/aggregate_fn/krum.yaml b/baselines/flanders/flanders/conf/aggregate_fn/krum.yaml new file mode 100644 index 000000000000..220b93d92b3e --- /dev/null +++ b/baselines/flanders/flanders/conf/aggregate_fn/krum.yaml @@ -0,0 +1,7 @@ +--- +name: krum + +aggregate_fn: + function: flwr.server.strategy.aggregate.aggregate_krum + parameters: + to_keep: 10 \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/aggregate_fn/trimmedmean.yaml b/baselines/flanders/flanders/conf/aggregate_fn/trimmedmean.yaml new file mode 100644 index 000000000000..d2e418fa9738 --- /dev/null +++ b/baselines/flanders/flanders/conf/aggregate_fn/trimmedmean.yaml @@ -0,0 +1,7 @@ +--- +name: trimmedmean + +aggregate_fn: + function: flwr.server.strategy.aggregate.aggregate_trimmed_avg + parameters: + proportiontocut: 0.4 \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/base.yaml b/baselines/flanders/flanders/conf/base.yaml new file mode 100644 index 000000000000..9742d85e2af8 --- /dev/null +++ b/baselines/flanders/flanders/conf/base.yaml @@ -0,0 +1,27 @@ +defaults: + - _self_ + - strategy: fedavg + - aggregate_fn: fedavg + +dataset: mnist + +server: + _target_: flanders.server.EnhancedServer + num_rounds: 100 + pool_size: 100 + warmup_rounds: 2 + sampling: 500 + history_dir: clients_params + magnitude: 10 + threshold: 1e-05 + attack_fn: gaussian + num_malicious: 0 + omniscent: True + noniidness: 0.5 + +server_device: cpu +seed: 33 + +client_resources: + num_cpus: 1 + num_gpus: 0 \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/bulyan.yaml b/baselines/flanders/flanders/conf/strategy/bulyan.yaml new file mode 100644 index 000000000000..1692d5d4306c --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/bulyan.yaml @@ -0,0 +1,8 @@ +--- +name: bulyan + +strategy: + _target_: flwr.server.strategy.Bulyan + _recursive_: true + num_malicious_clients: $(server.num_malicious) + to_keep: 0 # Normal Krum is applied \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/fedavg.yaml b/baselines/flanders/flanders/conf/strategy/fedavg.yaml new file mode 100644 index 000000000000..1be4b0a0cc5b --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/fedavg.yaml @@ -0,0 +1,5 @@ +--- +name: fedavg + +strategy: + _target_: flwr.server.strategy.FedAvg \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/fedmedian.yaml b/baselines/flanders/flanders/conf/strategy/fedmedian.yaml new file mode 100644 index 000000000000..d79293f4ca23 --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/fedmedian.yaml @@ -0,0 +1,5 @@ +--- +name: fedmedian + +strategy: + _target_: flwr.server.strategy.FedMedian \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/flanders.yaml b/baselines/flanders/flanders/conf/strategy/flanders.yaml new file mode 100644 index 000000000000..0222708dd836 --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/flanders.yaml @@ -0,0 +1,10 @@ +--- +name: flanders + +strategy: + _target_: flanders.strategy.Flanders + _recursive_: true + num_clients_to_keep: 3 # number of benign local models to filter-out before the aggregation (atm it's set to be pool_size - num_malicious, hard coded in main.py) + maxiter: 100 # number of iterations done by MAR + alpha: 1 + beta: 1 \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/krum.yaml b/baselines/flanders/flanders/conf/strategy/krum.yaml new file mode 100644 index 000000000000..bc36d37755fa --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/krum.yaml @@ -0,0 +1,7 @@ +--- +name: krum + +strategy: + _target_: flwr.server.strategy.Krum + num_clients_to_keep: 3 + num_malicious_clients: ${server.num_malicious} \ No newline at end of file diff --git a/baselines/flanders/flanders/conf/strategy/trimmedmean.yaml b/baselines/flanders/flanders/conf/strategy/trimmedmean.yaml new file mode 100644 index 000000000000..561755f82d35 --- /dev/null +++ b/baselines/flanders/flanders/conf/strategy/trimmedmean.yaml @@ -0,0 +1,6 @@ +--- +name: trimmedmean + +strategy: + _target_: flwr.server.strategy.FedTrimmedAvg + beta: 0.2 \ No newline at end of file diff --git a/baselines/flanders/flanders/dataset.py b/baselines/flanders/flanders/dataset.py new file mode 100644 index 000000000000..2c13e80d75c5 --- /dev/null +++ b/baselines/flanders/flanders/dataset.py @@ -0,0 +1,289 @@ +"""Dataset utilities for FL experiments.""" + +# Borrowed from adap/Flower examples + +import shutil +from pathlib import Path +from typing import Any, Callable, Optional, Tuple + +import numpy as np +import torch +from PIL import Image +from torch.utils.data import DataLoader, SubsetRandomSampler +from torchvision import datasets, transforms +from torchvision.datasets import VisionDataset + +from .dataset_preparation import create_lda_partitions + + +class Data(torch.utils.data.Dataset): + """Dataset class.""" + + def __init__(self, X, y): + """Initialize dataset.""" + self.X = torch.from_numpy(X.astype(np.float32)) + self.y = torch.from_numpy(y.astype(np.float32)) + self.len = self.X.shape[0] + + def __getitem__(self, index): + """Return data and label pair.""" + return self.X[index], self.y[index] + + def __len__(self): + """Return size of dataset.""" + return self.len + + +def get_dataset(path_to_data: Path, cid: str, partition: str, transform=None): + """Return TorchVisionFL dataset object.""" + # generate path to cid's data + path_to_data = path_to_data / cid / (partition + ".pt") + + return TorchVisionFL(path_to_data, transform=transform) + + +# pylint: disable=too-many-arguments, too-many-locals +def get_dataloader( + path_to_data: str, + cid: str, + is_train: bool, + batch_size: int, + workers: int, + transform=None, +): + """Generate trainset/valset object and returns appropiate dataloader.""" + partition = "train" if is_train else "val" + dataset = get_dataset(Path(path_to_data), str(cid), partition, transform=transform) + + # we use as number of workers all the cpu cores assigned to this actor + kwargs = {"num_workers": workers, "pin_memory": True, "drop_last": False} + return DataLoader(dataset, batch_size=batch_size, **kwargs) + + +def get_random_id_splits(total: int, val_ratio: float, shuffle: bool = True): + """Random split. + + Split a list of length `total` into two following a (1-val_ratio):val_ratio + partitioning. + + By default the indices are shuffled before creating the split and returning. + """ + if isinstance(total, int): + indices = list(range(total)) + else: + indices = total + + split = int(np.floor(val_ratio * len(indices))) + # print(f"Users left out for validation (ratio={val_ratio}) = {split} ") + if shuffle: + np.random.shuffle(indices) + return indices[split:], indices[:split] + + +# pylint: disable=too-many-arguments, too-many-locals +def do_fl_partitioning( + path_to_dataset, pool_size, alpha, num_classes, val_ratio=0.0, seed=None +): + """Torchvision (e.g. CIFAR-10) datasets using LDA.""" + images, labels = torch.load(path_to_dataset) + idx = np.array(range(len(images))) + dataset = [idx, labels] + partitions, _ = create_lda_partitions( + dataset, + num_partitions=pool_size, + concentration=alpha, + accept_imbalanced=True, + seed=seed, + ) + + # Show label distribution for first partition (purely informative) + partition_zero = partitions[0][1] + hist, _ = np.histogram(partition_zero, bins=list(range(num_classes + 1))) + print( + "Class histogram for 0-th partition" + f"(alpha={alpha}, {num_classes} classes): {hist}" + ) + + # now save partitioned dataset to disk + # first delete dir containing splits (if exists), then create it + splits_dir = path_to_dataset.parent / "federated" + if splits_dir.exists(): + shutil.rmtree(splits_dir) + Path.mkdir(splits_dir, parents=True) + + for idx in range(pool_size): + labels = partitions[idx][1] + image_idx = partitions[idx][0] + imgs = images[image_idx] + + # create dir + Path.mkdir(splits_dir / str(idx)) + + if val_ratio > 0.0: + # split data according to val_ratio + train_idx, val_idx = get_random_id_splits(len(labels), val_ratio) + val_imgs = imgs[val_idx] + val_labels = labels[val_idx] + + with open(splits_dir / str(idx) / "val.pt", "wb") as fil: + torch.save([val_imgs, val_labels], fil) + + # remaining images for training + imgs = imgs[train_idx] + labels = labels[train_idx] + + with open(splits_dir / str(idx) / "train.pt", "wb") as fil: + torch.save([imgs, labels], fil) + + return splits_dir + + +def mnist_transformation(img): + """Return TorchVision transformation for MNIST.""" + return transforms.Compose( + [ + transforms.ToTensor(), + transforms.Normalize(mean=(0.5,), std=(0.5,)), + ] + )(img) + + +class TorchVisionFL(VisionDataset): + """TorchVision FL class. + + Use this class by either passing a path to a torch file (.pt) containing (data, + targets) or pass the data, targets directly instead. + + This is just a trimmed down version of torchvision.datasets.MNIST. + """ + + def __init__( + self, + path_to_data=None, + data=None, + targets=None, + transform: Optional[Callable] = None, + ) -> None: + """Initialize dataset.""" + path = path_to_data.parent if path_to_data else None + super().__init__(path, transform=transform) + self.transform = transform + + if path_to_data: + # load data and targets (path_to_data points to an specific .pt file) + self.data, self.targets = torch.load(path_to_data) + else: + self.data = data + self.targets = targets + + def __getitem__(self, index: int) -> Tuple[Any, Any]: + """Return a tuple (data, target).""" + img, target = self.data[index], int(self.targets[index]) + + # doing this so that it is consistent with all other datasets + # to return a PIL Image + if not isinstance(img, Image.Image): # if not PIL image + if not isinstance(img, np.ndarray): # if torch tensor + img = img.numpy() + + img = Image.fromarray(img) + + if self.transform is not None: + img = self.transform(img) + + if self.target_transform is not None: + target = self.target_transform(target) + + return img, target + + def __len__(self) -> int: + """Return length of dataset.""" + return len(self.data) + + +def get_mnist(path_to_data="flanders/datasets_files/mnist/data"): + """Download MNIST dataset.""" + # download dataset and load train set + train_set = datasets.MNIST(root=path_to_data, train=True, download=True) + + # fuse all data splits into a single "training.pt" + data_loc = Path(path_to_data) / "MNIST" + training_data = data_loc / "training.pt" + print("Generating unified MNIST dataset") + torch.save([train_set.data, np.array(train_set.targets)], training_data) + + test_set = datasets.MNIST( + root=path_to_data, train=False, transform=mnist_transformation + ) + + # returns path where training data is and testset + return training_data, test_set + + +def get_fmnist(path_to_data="flanders/datasets_files/fmnist/data"): + """Download FashionMNIST dataset.""" + # download dataset and load train set + train_set = datasets.FashionMNIST(root=path_to_data, train=True, download=True) + + # fuse all data splits into a single "training.pt" + data_loc = Path(path_to_data) / "FashionMNIST" + training_data = data_loc / "training.pt" + print("Generating unified FashionMNIST dataset") + torch.save([train_set.data, np.array(train_set.targets)], training_data) + + test_set = datasets.FashionMNIST( + root=path_to_data, train=False, transform=mnist_transformation + ) + + # returns path where training data is and testset + return training_data, test_set + + +def dataset_partitioner( + dataset: torch.utils.data.Dataset, + batch_size: int, + client_id: int, + number_of_clients: int, + workers: int = 1, +) -> torch.utils.data.DataLoader: + """Make datasets partitions for a specific client_id. + + Parameters + ---------- + dataset: torch.utils.data.Dataset + Dataset to be partitioned into *number_of_clients* subsets. + batch_size: int + Size of mini-batches used by the returned DataLoader. + client_id: int + Unique integer used for selecting a specific partition. + number_of_clients: int + Total number of clients launched during training. + This value dictates the number of partitions to be created. + + Returns + ------- + data_loader: torch.utils.data.Dataset + DataLoader for specific client_id considering number_of_clients partitions. + """ + # Set the seed so we are sure to generate the same global batches + # indices across all clients + np.random.seed(123) + + # Get the data corresponding to this client + dataset_size = len(dataset) + nb_samples_per_clients = dataset_size // number_of_clients + dataset_indices = list(range(dataset_size)) + np.random.shuffle(dataset_indices) + + # Get starting and ending indices w.r.t CLIENT_ID + start_ind = int(client_id) * nb_samples_per_clients + end_ind = start_ind + nb_samples_per_clients + data_sampler = SubsetRandomSampler(dataset_indices[start_ind:end_ind]) + data_loader = torch.utils.data.DataLoader( + dataset, + batch_size=batch_size, + shuffle=False, + sampler=data_sampler, + num_workers=workers, + ) + return data_loader diff --git a/baselines/flanders/flanders/dataset_preparation.py b/baselines/flanders/flanders/dataset_preparation.py new file mode 100644 index 000000000000..3c1cfbe6a5d2 --- /dev/null +++ b/baselines/flanders/flanders/dataset_preparation.py @@ -0,0 +1,490 @@ +# Copyright 2020 Adap GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Commonly used functions for generating partitioned datasets.""" + +# pylint: disable=invalid-name + +from typing import List, Optional, Tuple, Union + +import numpy as np +from numpy.random import BitGenerator, Generator, SeedSequence + +XY = Tuple[np.ndarray, np.ndarray] +XYList = List[XY] +PartitionedDataset = Tuple[XYList, XYList] + + +def float_to_int(i: float) -> int: + """Return float as int but raise if decimal is dropped.""" + if not i.is_integer(): + raise Exception("Cast would drop decimals") + + return int(i) + + +def sort_by_label(x: np.ndarray, y: np.ndarray) -> XY: + """Sort by label. + + Assuming two labels and four examples the resulting label order would be 1,1,2,2 + """ + idx = np.argsort(y, axis=0).reshape((y.shape[0])) + return (x[idx], y[idx]) + + +def sort_by_label_repeating(x: np.ndarray, y: np.ndarray) -> XY: + """Sort by label in repeating groups. + + Assuming two labels and four examples the resulting label order would be 1,2,1,2. + + Create sorting index which is applied to by label sorted x, y + + .. code-block:: python + + # given: + y = [ + 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9 + ] + + # use: + idx = [ + 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 + ] + + # so that y[idx] becomes: + y = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + ] + """ + x, y = sort_by_label(x, y) + + num_example = x.shape[0] + num_class = np.unique(y).shape[0] + idx = ( + np.array(range(num_example), np.int64) + .reshape((num_class, num_example // num_class)) + .transpose() + .reshape(num_example) + ) + + return (x[idx], y[idx]) + + +def split_at_fraction(x: np.ndarray, y: np.ndarray, fraction: float) -> Tuple[XY, XY]: + """Split x, y at a certain fraction.""" + splitting_index = float_to_int(x.shape[0] * fraction) + # Take everything BEFORE splitting_index + x_0, y_0 = x[:splitting_index], y[:splitting_index] + # Take everything AFTER splitting_index + x_1, y_1 = x[splitting_index:], y[splitting_index:] + return (x_0, y_0), (x_1, y_1) + + +def shuffle(x: np.ndarray, y: np.ndarray) -> XY: + """Shuffle x and y.""" + idx = np.random.permutation(len(x)) + return x[idx], y[idx] + + +def partition(x: np.ndarray, y: np.ndarray, num_partitions: int) -> List[XY]: + """Return x, y as list of partitions.""" + return list(zip(np.split(x, num_partitions), np.split(y, num_partitions))) + + +def combine_partitions(xy_list_0: XYList, xy_list_1: XYList) -> XYList: + """Combine two lists of ndarray Tuples into one list.""" + return [ + (np.concatenate([x_0, x_1], axis=0), np.concatenate([y_0, y_1], axis=0)) + for (x_0, y_0), (x_1, y_1) in zip(xy_list_0, xy_list_1) + ] + + +def create_partitions( + unpartitioned_dataset: XY, + iid_fraction: float, + num_partitions: int, +) -> XYList: + """Create partitioned version of a training or test set. + + Currently tested and supported are MNIST and FashionMNIST + """ + x, y = unpartitioned_dataset + + x, y = shuffle(x, y) + x, y = sort_by_label_repeating(x, y) + + (x_0, y_0), (x_1, y_1) = split_at_fraction(x, y, fraction=iid_fraction) + + # Shift in second split of dataset the classes into two groups + x_1, y_1 = _shift(x_1, y_1) + + xy_0_partitions = partition(x_0, y_0, num_partitions) + xy_1_partitions = partition(x_1, y_1, num_partitions) + + xy_partitions = combine_partitions(xy_0_partitions, xy_1_partitions) + + # Adjust x and y shape + return [adjust_xy_shape(xy) for xy in xy_partitions] + + +def create_partitioned_dataset( + keras_dataset: Tuple[XY, XY], + iid_fraction: float, + num_partitions: int, +) -> Tuple[PartitionedDataset, XY]: + """Create partitioned version of keras dataset. + + Currently tested and supported are MNIST and FashionMNIST + """ + xy_train, xy_test = keras_dataset + + xy_train_partitions = create_partitions( + unpartitioned_dataset=xy_train, + iid_fraction=iid_fraction, + num_partitions=num_partitions, + ) + + xy_test_partitions = create_partitions( + unpartitioned_dataset=xy_test, + iid_fraction=iid_fraction, + num_partitions=num_partitions, + ) + + return (xy_train_partitions, xy_test_partitions), adjust_xy_shape(xy_test) + + +def log_distribution(xy_partitions: XYList) -> None: + """Print label distribution for list of paritions.""" + distro = [np.unique(y, return_counts=True) for _, y in xy_partitions] + for d in distro: + print(d) + + +def adjust_xy_shape(xy: XY) -> XY: + """Adjust shape of both x and y.""" + x, y = xy + if x.ndim == 3: + x = adjust_x_shape(x) + if y.ndim == 2: + y = adjust_y_shape(y) + return (x, y) + + +def adjust_x_shape(nda: np.ndarray) -> np.ndarray: + """Turn shape (x, y, z) into (x, y, z, 1).""" + nda_adjusted = np.reshape(nda, (nda.shape[0], nda.shape[1], nda.shape[2], 1)) + return nda_adjusted + + +def adjust_y_shape(nda: np.ndarray) -> np.ndarray: + """Turn shape (x, 1) into (x).""" + nda_adjusted = np.reshape(nda, (nda.shape[0])) + return nda_adjusted + + +def split_array_at_indices( + x: np.ndarray, split_idx: np.ndarray +) -> List[List[np.ndarray]]: + """Split an array `x` into list of elements using starting indices from `split_idx`. + + This function should be used with `unique_indices` from `np.unique()` after + sorting by label. + + Args: + x (np.ndarray): Original array of dimension (N,a,b,c,...) + split_idx (np.ndarray): 1-D array contaning increasing number of + indices to be used as partitions. Initial value must be zero. Last value + must be less than N. + + Returns + ------- + List[List[np.ndarray]]: List of list of samples. + """ + if split_idx.ndim != 1: + raise ValueError("Variable `split_idx` must be a 1-D numpy array.") + if split_idx.dtype != np.int64: + raise ValueError("Variable `split_idx` must be of type np.int64.") + if split_idx[0] != 0: + raise ValueError("First value of `split_idx` must be 0.") + if split_idx[-1] >= x.shape[0]: + raise ValueError( + """Last value in `split_idx` must be less than + the number of samples in `x`.""" + ) + if not np.all(split_idx[:-1] <= split_idx[1:]): + raise ValueError("Items in `split_idx` must be in increasing order.") + + num_splits: int = len(split_idx) + split_idx = np.append(split_idx, x.shape[0]) + + list_samples_split: List[List[np.ndarray]] = [[] for _ in range(num_splits)] + for j in range(num_splits): + tmp_x = x[split_idx[j] : split_idx[j + 1]] # noqa: E203 + for sample in tmp_x: + list_samples_split[j].append(sample) + + return list_samples_split + + +def exclude_classes_and_normalize( + distribution: np.ndarray, exclude_dims: List[bool], eps: float = 1e-5 +) -> np.ndarray: + """Exclude classes from a distribution. + + This function is particularly useful when sampling without replacement. + Classes for which no sample is available have their probabilities are set to 0. + Classes that had probabilities originally set to 0 are incremented with + `eps` to allow sampling from remaining items. + + Args: + distribution (np.array): Distribution being used. + exclude_dims (List[bool]): Dimensions to be excluded. + eps (float, optional): Small value to be addad to non-excluded dimensions. + Defaults to 1e-5. + + Returns + ------- + np.ndarray: Normalized distributions. + """ + if np.any(distribution < 0) or (not np.isclose(np.sum(distribution), 1.0)): + raise ValueError("distribution must sum to 1 and have only positive values.") + + if distribution.size != len(exclude_dims): + raise ValueError( + """Length of distribution must be equal + to the length `exclude_dims`.""" + ) + if eps < 0: + raise ValueError("""The value of `eps` must be positive and small.""") + + distribution[[not x for x in exclude_dims]] += eps + distribution[exclude_dims] = 0.0 + sum_rows = np.sum(distribution) + np.finfo(float).eps + distribution = distribution / sum_rows + + return distribution + + +def sample_without_replacement( + distribution: np.ndarray, + list_samples: List[List[np.ndarray]], + num_samples: int, + empty_classes: List[bool], +) -> Tuple[XY, List[bool]]: + """Sample from a list without replacement using a given distribution. + + Args: + distribution (np.ndarray): Distribution used for sampling. + list_samples(List[List[np.ndarray]]): List of samples. + num_samples (int): Total number of items to be sampled. + empty_classes (List[bool]): List of booleans indicating which classes are empty. + This is useful to differentiate which classes should still be sampled. + + Returns + ------- + XY: Dataset contaning samples + List[bool]: empty_classes. + """ + if np.sum([len(x) for x in list_samples]) < num_samples: + raise ValueError( + """Number of samples in `list_samples` is less than `num_samples`""" + ) + + # Make sure empty classes are not sampled + # and solves for rare cases where + if not empty_classes: + empty_classes = len(distribution) * [False] + + distribution = exclude_classes_and_normalize( + distribution=distribution, exclude_dims=empty_classes + ) + + data: List[np.ndarray] = [] + target: List[np.ndarray] = [] + + for _ in range(num_samples): + sample_class = np.where(np.random.multinomial(1, distribution) == 1)[0][0] + sample: np.ndarray = list_samples[sample_class].pop() + + data.append(sample) + target.append(sample_class) + + # If last sample of the class was drawn, then set the + # probability density function (PDF) to zero for that class. + if len(list_samples[sample_class]) == 0: + empty_classes[sample_class] = True + # Be careful to distinguish between classes that had zero probability + # and classes that are now empty + distribution = exclude_classes_and_normalize( + distribution=distribution, exclude_dims=empty_classes + ) + data_array: np.ndarray = np.concatenate([data], axis=0) + target_array: np.ndarray = np.array(target, dtype=np.int64) + + return (data_array, target_array), empty_classes + + +def get_partitions_distributions(partitions: XYList) -> Tuple[np.ndarray, List[int]]: + """Evaluate the distribution over classes for a set of partitions. + + Args: + partitions (XYList): Input partitions + + Returns + ------- + np.ndarray: Distributions of size (num_partitions, num_classes) + """ + # Get largest available label + labels = set() + for _, y in partitions: + labels.update(set(y)) + list_labels = sorted(labels) + bin_edges = np.arange(len(list_labels) + 1) + + # Pre-allocate distributions + distributions = np.zeros((len(partitions), len(list_labels)), dtype=np.float32) + for idx, (_, _y) in enumerate(partitions): + hist, _ = np.histogram(_y, bin_edges) + distributions[idx] = hist / hist.sum() + + return distributions, list_labels + + +def create_lda_partitions( + dataset: XY, + dirichlet_dist: Optional[np.ndarray] = None, + num_partitions: int = 100, + concentration: Union[float, np.ndarray, List[float]] = 0.5, + accept_imbalanced: bool = False, + seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None, +) -> Tuple[XYList, np.ndarray]: + r"""Create imbalanced non-iid partitions. + + Create imbalanced non-iid partitions using Latent Dirichlet Allocation (LDA) + without resampling. + + Args: + dataset (XY): Dataset containing samples X and labels Y. + dirichlet_dist (numpy.ndarray, optional): previously generated distribution to + be used. This is useful when applying the same distribution for train and + validation sets. + num_partitions (int, optional): Number of partitions to be created. + Defaults to 100. + concentration (float, np.ndarray, List[float]): Dirichlet Concentration + (:math:`\\alpha`) parameter. Set to float('inf') to get uniform partitions. + An :math:`\\alpha \\to \\Inf` generates uniform distributions over classes. + An :math:`\\alpha \\to 0.0` generates one class per client. Defaults to 0.5. + accept_imbalanced (bool): Whether or not to accept imbalanced output classes. + Default False. + seed (None, int, SeedSequence, BitGenerator, Generator): + A seed to initialize the BitGenerator for generating the Dirichlet + distribution. This is defined in Numpy's official documentation as follows: + If None, then fresh, unpredictable entropy will be pulled from the OS. + One may also pass in a SeedSequence instance. + Additionally, when passed a BitGenerator, it will be wrapped by Generator. + If passed a Generator, it will be returned unaltered. + See official Numpy Documentation for further details. + + Returns + ------- + Tuple[XYList, numpy.ndarray]: List of XYList containing partitions + for each dataset and the dirichlet probability density functions. + """ + # pylint: disable=too-many-arguments,too-many-locals + + x, y = dataset + x, y = shuffle(x, y) + x, y = sort_by_label(x, y) + + if (x.shape[0] % num_partitions) and (not accept_imbalanced): + raise ValueError( + """Total number of samples must be a multiple of `num_partitions`. + If imbalanced classes are allowed, set + `accept_imbalanced=True`.""" + ) + + num_samples = num_partitions * [0] + for j in range(x.shape[0]): + num_samples[j % num_partitions] += 1 + + # Get number of classes and verify if they matching with + classes, start_indices = np.unique(y, return_index=True) + + # Make sure that concentration is np.array and + # check if concentration is appropriate + concentration = np.asarray(concentration) + + # Check if concentration is Inf, if so create uniform partitions + partitions: List[XY] = [(_, _) for _ in range(num_partitions)] + if float("inf") in concentration: + partitions = create_partitions( + unpartitioned_dataset=(x, y), + iid_fraction=1.0, + num_partitions=num_partitions, + ) + dirichlet_dist = get_partitions_distributions(partitions)[0] + + return partitions, dirichlet_dist + + if concentration.size == 1: + concentration = np.repeat(concentration, classes.size) + elif concentration.size != classes.size: # Sequence + raise ValueError( + f"The size of the provided concentration ({concentration.size}) ", + f"must be either 1 or equal number of classes {classes.size})", + ) + + # Split into list of list of samples per class + list_samples_per_class: List[List[np.ndarray]] = split_array_at_indices( + x, start_indices + ) + + if dirichlet_dist is None: + dirichlet_dist = np.random.default_rng(seed).dirichlet( + alpha=concentration, size=num_partitions + ) + + if dirichlet_dist.size != 0: + if dirichlet_dist.shape != (num_partitions, classes.size): + raise ValueError( + f"""The shape of the provided dirichlet distribution + ({dirichlet_dist.shape}) must match the provided number + of partitions and classes ({num_partitions},{classes.size})""" + ) + + # Assuming balanced distribution + empty_classes = classes.size * [False] + for partition_id in range(num_partitions): + partitions[partition_id], empty_classes = sample_without_replacement( + distribution=dirichlet_dist[partition_id].copy(), + list_samples=list_samples_per_class, + num_samples=num_samples[partition_id], + empty_classes=empty_classes, + ) + + return partitions, dirichlet_dist + + +def _shift(x: np.ndarray, y: np.ndarray) -> XY: + """Shift data. + + Shift x_1, y_1 so that the first half contains only labels 0 to 4 and the second + half 5 to 9. + """ + x, y = sort_by_label(x, y) + + (x_0, y_0), (x_1, y_1) = split_at_fraction(x, y, fraction=0.5) + (x_0, y_0), (x_1, y_1) = shuffle(x_0, y_0), shuffle(x_1, y_1) + x, y = np.concatenate([x_0, x_1], axis=0), np.concatenate([y_0, y_1], axis=0) + return x, y diff --git a/baselines/flanders/flanders/main.py b/baselines/flanders/flanders/main.py new file mode 100644 index 000000000000..022c38b1ef32 --- /dev/null +++ b/baselines/flanders/flanders/main.py @@ -0,0 +1,279 @@ +"""FLANDERS main scrip.""" + +import importlib +import os +import random +import shutil + +import flwr as fl +import hydra +import numpy as np +import pandas as pd +import torch +from flwr.server.client_manager import SimpleClientManager +from hydra.core.hydra_config import HydraConfig +from hydra.utils import instantiate +from omegaconf import DictConfig, OmegaConf + +from .attacks import fang_attack, gaussian_attack, lie_attack, minmax_attack, no_attack +from .client import FMnistClient, MnistClient +from .dataset import do_fl_partitioning, get_fmnist, get_mnist +from .server import EnhancedServer +from .utils import fmnist_evaluate, l2_norm, mnist_evaluate + + +# pylint: disable=too-many-locals, too-many-branches, too-many-statements +@hydra.main(config_path="conf", config_name="base", version_base=None) +def main(cfg: DictConfig) -> None: + """Run the baseline. + + Parameters + ---------- + cfg : DictConfig + An omegaconf object that stores the hydra config. + """ + # 0. Set random seed + seed = cfg.seed + np.random.seed(seed) + np.random.set_state( + np.random.RandomState(seed).get_state() # pylint: disable=no-member + ) + random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + + # 1. Print parsed config + print(OmegaConf.to_yaml(cfg)) + + # Skip if: + # - strategy = bulyan and num_malicious > 20 + # - attack_fn != gaussian and num_malicious = 0 + if cfg.strategy.name == "bulyan" and cfg.server.num_malicious > 20: + print( + "Skipping experiment because strategy is bulyan and num_malicious is > 20" + ) + return + # skip if attack_fn is not gaussian and num_malicious is 0, but continue if + # attack_fn is na + if ( + cfg.server.attack_fn != "gaussian" + and cfg.server.num_malicious == 0 + and cfg.server.attack_fn != "na" + ): + print( + "Skipping experiment because attack_fn is not gaussian and " + "num_malicious is 0" + ) + return + + attacks = { + "na": no_attack, + "gaussian": gaussian_attack, + "lie": lie_attack, + "fang": fang_attack, # OPT + "minmax": minmax_attack, # AGR-MM + } + + clients = { + "mnist": (MnistClient, mnist_evaluate), + "fmnist": (FMnistClient, fmnist_evaluate), + } + + # Delete old client_params + if os.path.exists(cfg.server.history_dir): + shutil.rmtree(cfg.server.history_dir) + + dataset_name = cfg.dataset + attack_fn = cfg.server.attack_fn + num_malicious = cfg.server.num_malicious + + # 2. Prepare your dataset + if dataset_name in ["mnist", "fmnist"]: + if dataset_name == "mnist": + train_path, _ = get_mnist() + elif dataset_name == "fmnist": + train_path, _ = get_fmnist() + fed_dir = do_fl_partitioning( + train_path, + pool_size=cfg.server.pool_size, + alpha=cfg.server.noniidness, + num_classes=10, + val_ratio=0.2, + seed=seed, + ) + else: + raise ValueError("Dataset not supported") + + # 3. Define your clients + # pylint: disable=no-else-return + def client_fn(cid: str, dataset_name: str = dataset_name): + client = clients[dataset_name][0] + if dataset_name in ["mnist", "fmnist"]: + return client(cid, fed_dir) + else: + raise ValueError("Dataset not supported") + + # 4. Define your strategy + strategy = None + if cfg.strategy.name == "flanders": + function_path = cfg.aggregate_fn.aggregate_fn.function + module_name, function_name = function_path.rsplit(".", 1) + module = importlib.import_module(module_name, package=__package__) + aggregation_fn = getattr(module, function_name) + + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + num_clients_to_keep=cfg.server.pool_size - num_malicious, + aggregate_fn=aggregation_fn, + aggregate_parameters=cfg.aggregate_fn.aggregate_fn.parameters, + min_available_clients=cfg.server.pool_size, + window=cfg.server.warmup_rounds, + distance_function=l2_norm, + maxiter=cfg.strategy.strategy.maxiter, + alpha=cfg.strategy.strategy.alpha, + beta=int(cfg.strategy.strategy.beta), + ) + elif cfg.strategy.name == "krum": + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + num_clients_to_keep=cfg.strategy.strategy.num_clients_to_keep, + min_available_clients=cfg.server.pool_size, + num_malicious_clients=num_malicious, + ) + elif cfg.strategy.name == "fedavg": + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + min_available_clients=cfg.server.pool_size, + ) + elif cfg.strategy.name == "bulyan": + # Get aggregation rule function + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + min_available_clients=cfg.server.pool_size, + num_malicious_clients=num_malicious, + to_keep=cfg.strategy.strategy.to_keep, + ) + elif cfg.strategy.name == "trimmedmean": + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + min_available_clients=cfg.server.pool_size, + beta=cfg.strategy.strategy.beta, + ) + elif cfg.strategy.name == "fedmedian": + strategy = instantiate( + cfg.strategy.strategy, + evaluate_fn=clients[dataset_name][1], + on_fit_config_fn=fit_config, + fraction_fit=1, + fraction_evaluate=0, + min_fit_clients=cfg.server.pool_size, + min_evaluate_clients=0, + min_available_clients=cfg.server.pool_size, + ) + else: + raise ValueError("Strategy not supported") + + # 5. Start Simulation + history = fl.simulation.start_simulation( + client_fn=client_fn, + num_clients=cfg.server.pool_size, + client_resources=cfg.client_resources, + server=EnhancedServer( + warmup_rounds=cfg.server.warmup_rounds, + num_malicious=num_malicious, + attack_fn=attacks[attack_fn], # type: ignore + magnitude=cfg.server.magnitude, + client_manager=SimpleClientManager(), + strategy=strategy, + sampling=cfg.server.sampling, + history_dir=cfg.server.history_dir, + dataset_name=dataset_name, + threshold=cfg.server.threshold, + omniscent=cfg.server.omniscent, + ), + config=fl.server.ServerConfig(num_rounds=cfg.server.num_rounds), + strategy=strategy, + ) + + save_path = HydraConfig.get().runtime.output_dir + + rounds, test_loss = zip(*history.losses_centralized) + _, test_accuracy = zip(*history.metrics_centralized["accuracy"]) + _, test_auc = zip(*history.metrics_centralized["auc"]) + _, truep = zip(*history.metrics_centralized["TP"]) + _, truen = zip(*history.metrics_centralized["TN"]) + _, falsep = zip(*history.metrics_centralized["FP"]) + _, falsen = zip(*history.metrics_centralized["FN"]) + + if not os.path.exists(os.path.join(save_path, "outputs")): + os.makedirs(os.path.join(save_path, "outputs")) + path_to_save = [os.path.join(save_path, "results.csv"), "outputs/all_results.csv"] + + for file_name in path_to_save: + data = pd.DataFrame( + { + "round": rounds, + "loss": test_loss, + "accuracy": test_accuracy, + "auc": test_auc, + "TP": truep, + "TN": truen, + "FP": falsep, + "FN": falsen, + "attack_fn": [attack_fn for _ in range(len(rounds))], + "dataset_name": [dataset_name for _ in range(len(rounds))], + "num_malicious": [num_malicious for _ in range(len(rounds))], + "strategy": [cfg.strategy.name for _ in range(len(rounds))], + "aggregate_fn": [ + cfg.aggregate_fn.aggregate_fn.function for _ in range(len(rounds)) + ], + } + ) + if os.path.exists(file_name): + data.to_csv(file_name, mode="a", header=False, index=False) + else: + data.to_csv(file_name, index=False, header=True) + + +# pylint: disable=unused-argument +def fit_config(server_round): + """Return a configuration with static batch size and (local) epochs.""" + config = { + "epochs": 1, # number of local epochs + "batch_size": 32, + } + return config + + +if __name__ == "__main__": + main() diff --git a/baselines/flanders/flanders/models.py b/baselines/flanders/flanders/models.py new file mode 100644 index 000000000000..2fd10f5496d3 --- /dev/null +++ b/baselines/flanders/flanders/models.py @@ -0,0 +1,164 @@ +"""Models for FLANDERS experiments.""" + +import itertools + +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import roc_auc_score +from sklearn.preprocessing import LabelBinarizer + + +def roc_auc_multiclass(y_true, y_pred): + """Compute the ROC AUC for multiclass classification.""" + l_b = LabelBinarizer() + l_b.fit(y_true) + y_true = l_b.transform(y_true) + y_pred = l_b.transform(y_pred) + return roc_auc_score(y_true, y_pred, multi_class="ovr") + + +class MnistNet(nn.Module): + """Neural network for MNIST classification.""" + + def __init__(self): + super().__init__() + self.fc1 = nn.Linear(28 * 28, 128) + self.fc2 = nn.Linear(128, 10) + + def forward(self, x): + """Forward pass through the network.""" + x = x.view(-1, 28 * 28) + x = F.relu(self.fc1(x)) + x = self.fc2(x) + return F.log_softmax(x, dim=1) + + +def train_mnist(model, dataloader, epochs, device): + """Train the network on the training set.""" + criterion = nn.CrossEntropyLoss() + optimizer = torch.optim.Adam(model.parameters(), lr=0.001) + + for epoch in range(epochs): + for i, (images, labels) in enumerate(dataloader): + images = images.view(-1, 28 * 28).to(device) + labels = labels.to(device) + + optimizer.zero_grad() + outputs = model(images) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + + if (i + 1) % 100 == 0: + print( + f"Epoch [{epoch+1}/{epochs}], " + f"Step [{i+1}/{len(dataloader)}], " + f"Loss: {loss.item():.4f}" + ) + + +# pylint: disable=too-many-locals +def test_mnist(model, dataloader, device): + """Validate the network on the entire test set.""" + loss = 0 + model.eval() + criterion = nn.CrossEntropyLoss() + y_true, y_pred = [], [] + with torch.no_grad(): + n_correct = 0 + n_samples = 0 + for images, labels in dataloader: + images = images.reshape(-1, 28 * 28).to(device) + labels = labels.to(device) + outputs = model(images) + # max returns (value ,index) + _, predicted = torch.max(outputs.data, 1) + n_samples += labels.size(0) + n_correct += (predicted == labels).sum().item() + loss += criterion(outputs, labels).item() + y_true.append(labels.cpu().numpy()) + y_pred.append(predicted.cpu().numpy()) + y_true = list(itertools.chain(*y_true)) + y_pred = list(itertools.chain(*y_pred)) + auc = roc_auc_multiclass(y_true, y_pred) + acc = n_correct / n_samples + return loss, acc, auc + + +class FMnistNet(nn.Module): + """Neural network for Fashion MNIST classification.""" + + def __init__(self): + super().__init__() + self.fc1 = nn.Linear(784, 256) + self.fc2 = nn.Linear(256, 128) + self.fc3 = nn.Linear(128, 64) + self.fc4 = nn.Linear(64, 10) + + # Dropout module with a 0.2 drop probability + self.dropout = nn.Dropout(p=0.2) + + def forward(self, x): + """Forward pass through the network.""" + # Flatten the input tensor + x = x.view(x.shape[0], -1) + # Set the activation functions + x = self.dropout(F.relu(self.fc1(x))) + x = self.dropout(F.relu(self.fc2(x))) + x = self.dropout(F.relu(self.fc3(x))) + x = F.log_softmax(self.fc4(x), dim=1) + + return x + + +def train_fmnist(model, dataloader, epochs, device): + """Train the network on the training set.""" + criterion = nn.NLLLoss(reduction="sum") + optimizer = torch.optim.Adam(model.parameters(), lr=0.003) + + for epoch in range(epochs): + for i, (images, labels) in enumerate(dataloader): + images = images.view(-1, 28 * 28).to(device) + labels = labels.to(device) + + optimizer.zero_grad() + outputs = model(images) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + + if (i + 1) % 100 == 0: + print( + f"Epoch [{epoch+1}/{epochs}], " + f"Step [{i+1}/{len(dataloader)}], " + f"Loss: {loss.item():.4f}" + ) + + +# pylint: disable=too-many-locals +def test_fmnist(model, dataloader, device): + """Validate the network on the entire test set.""" + loss = 0 + model.eval() + criterion = nn.NLLLoss(reduction="sum") + y_true, y_pred = [], [] + with torch.no_grad(): + n_correct = 0 + n_samples = 0 + for images, labels in dataloader: + images = images.reshape(-1, 28 * 28).to(device) + labels = labels.to(device) + outputs = model(images) + # max returns (value ,index) + _, predicted = torch.max(outputs.data, 1) + n_samples += labels.size(0) + n_correct += (predicted == labels).sum().item() + loss += criterion(outputs, labels).item() + y_true.append(labels.cpu().numpy()) + y_pred.append(predicted.cpu().numpy()) + y_true = list(itertools.chain(*y_true)) + y_pred = list(itertools.chain(*y_pred)) + auc = roc_auc_multiclass(y_true, y_pred) + acc = n_correct / n_samples + return loss, acc, auc diff --git a/baselines/flanders/flanders/server.py b/baselines/flanders/flanders/server.py new file mode 100644 index 000000000000..622aa890a966 --- /dev/null +++ b/baselines/flanders/flanders/server.py @@ -0,0 +1,384 @@ +"""Server with enhanced functionality. + +It can be used to simulate an attacker that controls a fraction of the clients and to +save the parameters of each client in its memory. +""" + +import timeit +from logging import DEBUG, INFO +from typing import Any, Callable, Dict, List, Tuple, Union + +import numpy as np +from flwr.common import DisconnectRes, EvaluateRes, FitRes, parameters_to_ndarrays +from flwr.common.logger import log +from flwr.server.client_proxy import ClientProxy +from flwr.server.history import History +from flwr.server.server import Server, fit_clients + +from .strategy import Flanders +from .utils import flatten_params, save_params, update_confusion_matrix + +FitResultsAndFailures = Tuple[ + List[Tuple[ClientProxy, FitRes]], + List[Union[Tuple[ClientProxy, FitRes], BaseException]], +] +EvaluateResultsAndFailures = Tuple[ + List[Tuple[ClientProxy, EvaluateRes]], + List[Union[Tuple[ClientProxy, EvaluateRes], BaseException]], +] +ReconnectResultsAndFailures = Tuple[ + List[Tuple[ClientProxy, DisconnectRes]], + List[Union[Tuple[ClientProxy, DisconnectRes], BaseException]], +] + + +class EnhancedServer(Server): + """Server with enhanced functionality.""" + + # pylint: disable=too-many-arguments,too-many-instance-attributes + def __init__( + self, + num_malicious: int, + warmup_rounds: int, + attack_fn: Callable, + dataset_name: str, + *args: Any, + threshold: float = 0.0, + to_keep: int = 1, + magnitude: float = 0.0, + sampling: int = 0, + history_dir: str = "clients_params", + omniscent: bool = True, + **kwargs: Any, + ) -> None: + """Create a new EnhancedServer instance. + + Parameters + ---------- + num_malicious : int + Number of malicious clients + warmup_rounds : int + Number of warmup rounds + attack_fn : Callable + Attack function to be used + dataset_name : str + Name of the dataset + threshold : float, optional + Threshold used by the attacks, by default 0.0 + to_keep : int, optional + Number of clients to keep (i.e., to classify as "good"), by default 1 + magnitude : float, optional + Magnitude of the Gaussian attack, by default 0.0 + sampling : int, optional + Number of parameters to sample, by default 0 + history_dir : str, optional + Directory where to save the parameters, by default "clients_params" + omniscent : bool, optional + Whether to use the omniscent attack, by default True + """ + super().__init__(*args, **kwargs) + self.num_malicious = num_malicious + self.warmup_rounds = warmup_rounds + self.attack_fn = attack_fn + self.sampling = sampling + self.aggregated_parameters: List = [] + self.params_indexes: List = [] + self.history_dir = history_dir + self.dataset_name = dataset_name + self.magnitude = magnitude + self.threshold = threshold + self.to_keep = to_keep + self.omniscent = omniscent + self.malicious_lst: List = [] + self.confusion_matrix = {"TP": 0, "TN": 0, "FP": 0, "FN": 0} + self.clients_state: Dict[str, bool] = {} + self.good_clients_idx: List[int] = [] + self.malicious_clients_idx: List[int] = [] + + # pylint: disable=too-many-locals + def fit(self, num_rounds, timeout): + """Run federated averaging for a number of rounds.""" + history = History() + + # Initialize parameters + log(INFO, "Initializing global parameters") + self.parameters = self._get_initial_parameters(timeout=timeout) + log(INFO, "Evaluating initial parameters") + res = self.strategy.evaluate(0, parameters=self.parameters) + + if res is not None: + log( + INFO, + "initial parameters (loss, other metrics): %s, %s", + res[0], + res[1], + ) + res[1]["TP"] = 0 + res[1]["TN"] = 0 + res[1]["FP"] = 0 + res[1]["FN"] = 0 + history.add_loss_centralized(server_round=0, loss=res[0]) + history.add_metrics_centralized(server_round=0, metrics=res[1]) + + # Run federated learning for num_rounds + log(INFO, "FL starting") + start_time = timeit.default_timer() + + for current_round in range(1, num_rounds + 1): + # Train model and replace previous global model + res_fit = self.fit_round( + server_round=current_round, + timeout=timeout, + ) + if res_fit is not None: + parameters_prime, fit_metrics, _ = res_fit # fit_metrics_aggregated + if parameters_prime: + self.parameters = parameters_prime + history.add_metrics_distributed_fit( + server_round=current_round, metrics=fit_metrics + ) + + # Evaluate model using strategy implementation + res_cen = self.strategy.evaluate(current_round, parameters=self.parameters) + if res_cen is not None: + loss_cen, metrics_cen = res_cen + # Update confusion matrix + if current_round > self.warmup_rounds: + self.confusion_matrix = update_confusion_matrix( + self.confusion_matrix, + self.clients_state, + self.malicious_clients_idx, + self.good_clients_idx, + ) + + for key, val in self.confusion_matrix.items(): + metrics_cen[key] = val + + log( + INFO, + "fit progress: (%s, %s, %s, %s)", + current_round, + loss_cen, + metrics_cen, + timeit.default_timer() - start_time, + ) + history.add_loss_centralized(server_round=current_round, loss=loss_cen) + history.add_metrics_centralized( + server_round=current_round, metrics=metrics_cen + ) + + # Evaluate model on a sample of available clients + res_fed = self.evaluate_round(server_round=current_round, timeout=timeout) + if res_fed is not None: + loss_fed, evaluate_metrics_fed, _ = res_fed + if loss_fed is not None: + history.add_loss_distributed( + server_round=current_round, loss=loss_fed + ) + history.add_metrics_distributed( + server_round=current_round, metrics=evaluate_metrics_fed + ) + + # Bookkeeping + end_time = timeit.default_timer() + elapsed = end_time - start_time + log(INFO, "FL finished in %s", elapsed) + return history + + # pylint: disable-msg=R0915 + def fit_round( + self, + server_round, + timeout, + ): + # pylint: disable-msg=R0912 + """Perform a single round of federated learning.""" + # Get clients and their respective instructions from strategy + client_instructions = self.strategy.configure_fit( + server_round=server_round, + parameters=self.parameters, + client_manager=self._client_manager, + ) + + if not client_instructions: + log(INFO, "fit_round %s: no clients selected, cancel", server_round) + return None + log( + DEBUG, + "fit_round %s: strategy sampled %s clients (out of %s)", + server_round, + len(client_instructions), + self._client_manager.num_available(), + ) + + # Randomly decide which client is malicious + size = self.num_malicious + if server_round <= self.warmup_rounds: + size = 0 + log(INFO, "Selecting %s malicious clients", size) + self.malicious_lst = np.random.choice( + [proxy.cid for proxy, _ in client_instructions], size=size, replace=False + ) + + # Create dict clients_state to keep track of malicious clients + # and send the information to the clients + clients_state = {} + for _, (proxy, ins) in enumerate(client_instructions): + clients_state[proxy.cid] = False + ins.config["malicious"] = False + if proxy.cid in self.malicious_lst: + clients_state[proxy.cid] = True + ins.config["malicious"] = True + + # Sort clients states + clients_state = {k: clients_state[k] for k in sorted(clients_state)} + log( + DEBUG, + "fit_round %s: malicious clients selected %s, clients_state %s", + server_round, + self.malicious_lst, + clients_state, + ) + + # Collect `fit` results from all clients participating in this round + results, failures = fit_clients( + client_instructions=client_instructions, + max_workers=self.max_workers, + timeout=timeout, + ) + log( + DEBUG, + "fit_round %s received %s results and %s failures", + server_round, + len(results), + len(failures), + ) + + # Save parameters of each client as time series + ordered_results = [0 for _ in range(len(results))] + for proxy, fitres in results: + params = flatten_params(parameters_to_ndarrays(fitres.parameters)) + if self.sampling > 0: + # if the sampling number is greater than the number of + # parameters, just sample all of them + self.sampling = min(self.sampling, len(params)) + if len(self.params_indexes) == 0: + # Sample a random subset of parameters + self.params_indexes = np.random.randint( + 0, len(params), size=self.sampling + ) + + params = params[self.params_indexes] + + save_params(params, fitres.metrics["cid"], params_dir=self.history_dir) + + # Re-arrange results in the same order as clients' cids impose + ordered_results[int(fitres.metrics["cid"])] = (proxy, fitres) + + log(INFO, "Clients state: %s", clients_state) + + # Initialize aggregated_parameters if it is the first round + if self.aggregated_parameters == []: + for key, val in clients_state.items(): + if val is False: + self.aggregated_parameters = parameters_to_ndarrays( + ordered_results[int(key)][1].parameters + ) + break + + # Apply attack function + # the server simulates an attacker that controls a fraction of the clients + if self.attack_fn is not None and server_round > self.warmup_rounds: + log(INFO, "Applying attack function") + results, _ = self.attack_fn( + ordered_results, + clients_state, + omniscent=self.omniscent, + magnitude=self.magnitude, + w_re=self.aggregated_parameters, + threshold=self.threshold, + d=len(self.aggregated_parameters), + dataset_name=self.dataset_name, + to_keep=self.to_keep, + malicious_num=self.num_malicious, + num_layers=len(self.aggregated_parameters), + ) + + # Update saved parameters time series after the attack + for _, fitres in results: + if clients_state[fitres.metrics["cid"]]: + if self.sampling > 0: + params = flatten_params( + parameters_to_ndarrays(fitres.parameters) + )[self.params_indexes] + else: + params = flatten_params( + parameters_to_ndarrays(fitres.parameters) + ) + log( + INFO, + "Saving parameters of client %s with shape %s after the attack", + fitres.metrics["cid"], + params.shape, + ) + save_params( + params, + fitres.metrics["cid"], + params_dir=self.history_dir, + remove_last=True, + ) + else: + results = ordered_results + + # Aggregate training results + log(INFO, "fit_round - Aggregating training results") + good_clients_idx = [] + malicious_clients_idx = [] + aggregated_result = self.strategy.aggregate_fit(server_round, results, failures) + if isinstance(self.strategy, Flanders): + parameters_aggregated, metrics_aggregated = aggregated_result + malicious_clients_idx = metrics_aggregated["malicious_clients_idx"] + good_clients_idx = metrics_aggregated["good_clients_idx"] + + log(INFO, "Malicious clients: %s", malicious_clients_idx) + + log(INFO, "clients_state: %s", clients_state) + + # For clients detected as malicious, replace the last params in + # their history with tha current global model, otherwise the + # forecasting in next round won't be reliable (see the paper for + # more details) + if server_round > self.warmup_rounds: + log(INFO, "Saving parameters of clients") + for idx in malicious_clients_idx: + if self.sampling > 0: + new_params = flatten_params( + parameters_to_ndarrays(parameters_aggregated) + )[self.params_indexes] + else: + new_params = flatten_params( + parameters_to_ndarrays(parameters_aggregated) + ) + + log( + INFO, + "Saving parameters of client %s with shape %s", + idx, + new_params.shape, + ) + save_params( + new_params, + idx, + params_dir=self.history_dir, + remove_last=True, + rrl=False, + ) + else: + # Aggregate training results + log(INFO, "fit_round - Aggregating training results") + parameters_aggregated, metrics_aggregated = aggregated_result + + self.clients_state = clients_state + self.good_clients_idx = good_clients_idx + self.malicious_clients_idx = malicious_clients_idx + return parameters_aggregated, metrics_aggregated, (results, failures) diff --git a/baselines/flanders/flanders/strategy.py b/baselines/flanders/flanders/strategy.py new file mode 100644 index 000000000000..36dbc1182653 --- /dev/null +++ b/baselines/flanders/flanders/strategy.py @@ -0,0 +1,375 @@ +"""FLANDERS strategy.""" + +import importlib +import typing +from logging import INFO, WARNING +from typing import Callable, Dict, List, Optional, Tuple, Union + +import numpy as np +from flwr.common import ( + FitIns, + FitRes, + MetricsAggregationFn, + NDArrays, + Parameters, + Scalar, + ndarrays_to_parameters, + parameters_to_ndarrays, +) +from flwr.common.logger import log +from flwr.server.client_manager import ClientManager +from flwr.server.client_proxy import ClientProxy +from flwr.server.strategy.aggregate import aggregate +from flwr.server.strategy.fedavg import FedAvg + +from .utils import load_all_time_series + +WARNING_MIN_AVAILABLE_CLIENTS_TOO_LOW = """ +Setting `min_available_clients` lower than `min_fit_clients` or +`min_evaluate_clients` can cause the server to fail when there are too few clients +connected to the server. `min_available_clients` must be set to a value larger +than or equal to the values of `min_fit_clients` and `min_evaluate_clients`. +""" + + +class Flanders(FedAvg): + """Aggregation function based on MAR. + + Take a look at the paper for more details about the parameters. + """ + + # pylint: disable=too-many-arguments,too-many-instance-attributes, too-many-locals + def __init__( + self, + fraction_fit: float = 1.0, + fraction_evaluate: float = 1.0, + min_fit_clients: int = 2, + min_evaluate_clients: int = 2, + min_available_clients: int = 2, + evaluate_fn: Optional[ + Callable[ + [int, NDArrays, Dict[str, Scalar]], + Optional[Tuple[float, Dict[str, Scalar]]], + ] + ] = None, + on_fit_config_fn: Optional[Callable[[int], Dict[str, Scalar]]] = None, + on_evaluate_config_fn: Optional[Callable[[int], Dict[str, Scalar]]] = None, + accept_failures: bool = True, + initial_parameters: Optional[Parameters] = None, + fit_metrics_aggregation_fn: Optional[MetricsAggregationFn] = None, + evaluate_metrics_aggregation_fn: Optional[MetricsAggregationFn] = None, + num_clients_to_keep: int = 1, + aggregate_fn: Callable = aggregate, + aggregate_parameters: Optional[Dict[str, Scalar]] = None, + window: int = 0, + maxiter: int = 100, + alpha: float = 1, + beta: float = 1, + distance_function=None, + ) -> None: + """Initialize FLANDERS. + + Parameters + ---------- + fraction_fit : float, optional + Fraction of clients used during the fit phase, by default 1.0 + fraction_evaluate : float, optional + Fraction of clients used during the evaluate phase, by default 1.0 + min_fit_clients : int, optional + Minimum number of clients used during the fit phase, by default 2 + min_evaluate_clients : int, optional + Minimum number of clients used during the evaluate phase, by + default 2 + min_available_clients : int, optional + Minimum number of clients available for training and evaluation, by + default 2 + evaluate_fn : Optional[Callable[[int, NDArrays, Dict[str, Scalar]], + Optional[Tuple[float, Dict[str, Scalar]]]]], optional + Evaluation function, by default None + on_fit_config_fn : Optional[Callable[[int], Dict[str, Scalar]]], + optional + Function to generate the config fed to the clients during the fit + phase, by default None + on_evaluate_config_fn : Optional[Callable[[int], Dict[str, Scalar]]], + optional + Function to generate the config fed to the clients during the + evaluate phase, by default None + accept_failures : bool, optional + Whether to accept failures from clients, by default True + initial_parameters : Optional[Parameters], optional + Initial model parameters, by default None + fit_metrics_aggregation_fn : Optional[MetricsAggregationFn], optional + Function to aggregate metrics during the fit phase, by default None + evaluate_metrics_aggregation_fn : Optional[MetricsAggregationFn], + optional + Function to aggregate metrics during the evaluate phase, by default + None + num_clients_to_keep : int, optional + Number of clients to keep (i.e., to classify as "good"), by default + 1 + aggregate_fn : Callable[[List[Tuple[NDArrays, int]]], NDArrays], + optional + Function to aggregate the parameters, by default FedAvg + window : int, optional + Sliding window size used as a "training set" of MAR, by default 0 + maxiter : int, optional + Maximum number of iterations of MAR, by default 100 + alpha : float, optional + Alpha parameter (regularization), by default 1 + beta : float, optional + Beta parameter (regularization), by default 1 + distance_function : Callable, optional + Distance function used to compute the distance between predicted + params and real ones, by default None + """ + super().__init__( + fraction_fit=fraction_fit, + fraction_evaluate=fraction_evaluate, + min_fit_clients=min_fit_clients, + min_evaluate_clients=min_evaluate_clients, + min_available_clients=min_available_clients, + evaluate_fn=evaluate_fn, + on_fit_config_fn=on_fit_config_fn, + on_evaluate_config_fn=on_evaluate_config_fn, + accept_failures=accept_failures, + initial_parameters=initial_parameters, + fit_metrics_aggregation_fn=fit_metrics_aggregation_fn, + evaluate_metrics_aggregation_fn=evaluate_metrics_aggregation_fn, + ) + self.num_clients_to_keep = num_clients_to_keep + self.window = window + self.maxiter = maxiter + self.alpha = alpha + self.beta = beta + self.params_indexes = None + self.distance_function = distance_function + self.aggregate_fn = aggregate_fn + self.aggregate_parameters = aggregate_parameters + if self.aggregate_parameters is None: + self.aggregate_parameters = {} + + @typing.no_type_check + def configure_fit( + self, server_round: int, parameters: Parameters, client_manager: ClientManager + ) -> List[Tuple[ClientProxy, FitIns]]: + """Configure the next round of training.""" + # Sample clients + sample_size, min_num_clients = self.num_fit_clients( + client_manager.num_available() + ) + + # Custom FitIns object for each client + fit_ins_list = [ + FitIns( + parameters, + ( + {} + if not self.on_fit_config_fn + else self.on_fit_config_fn(server_round) + ), + ) + for _ in range(sample_size) + ] + + clients = client_manager.sample( + num_clients=sample_size, min_num_clients=min_num_clients + ) + + # Return client/config pairs + result = [] + for client, fit in zip(clients, fit_ins_list): + result.append((client, fit)) + return result + + # pylint: disable=too-many-locals,too-many-statements + @typing.no_type_check + def aggregate_fit( + self, + server_round: int, + results: List[Tuple[ClientProxy, FitRes]], + failures: List[Union[Tuple[ClientProxy, FitRes], BaseException]], + ) -> Tuple[Optional[Parameters], Dict[str, Scalar]]: + """Apply MAR forecasting to exclude malicious clients from FedAvg. + + Parameters + ---------- + server_round : int + Current server round. + results : List[Tuple[ClientProxy, FitRes]] + List of results from the clients. + failures : List[Union[Tuple[ClientProxy, FitRes], BaseException]] + List of failures from the clients. + + Returns + ------- + parameters_aggregated: Optional[Parameters] + Aggregated parameters. + metrics_aggregated: Dict[str, Scalar] + Aggregated metrics. + malicious_clients_idx: List[int] + List of malicious clients' cids (indexes). + """ + good_clients_idx = [] + malicious_clients_idx = [] + if server_round > 1: + if server_round < self.window: + self.window = server_round + params_tensor = load_all_time_series( + params_dir="clients_params", window=self.window + ) + params_tensor = np.transpose( + params_tensor, (0, 2, 1) + ) # (clients, params, time) + ground_truth = params_tensor[:, :, -1].copy() + pred_step = 1 + log(INFO, "Computing MAR on params_tensor %s", params_tensor.shape) + predicted_matrix = mar( + params_tensor[:, :, :-1], + pred_step, + maxiter=self.maxiter, + alpha=self.alpha, + beta=self.beta, + ) + + log(INFO, "Computing anomaly scores") + anomaly_scores = self.distance_function( + ground_truth, predicted_matrix[:, :, 0] + ) + log(INFO, "Anomaly scores: %s", anomaly_scores) + + log(INFO, "Selecting good clients") + good_clients_idx = sorted( + np.argsort(anomaly_scores)[: self.num_clients_to_keep] + ) # noqa + malicious_clients_idx = sorted( + np.argsort(anomaly_scores)[self.num_clients_to_keep :] + ) # noqa + + avg_anomaly_score_gc = np.mean(anomaly_scores[good_clients_idx]) + log( + INFO, "Average anomaly score for good clients: %s", avg_anomaly_score_gc + ) + + avg_anomaly_score_m = np.mean(anomaly_scores[malicious_clients_idx]) + log( + INFO, + "Average anomaly score for malicious clients: %s", + avg_anomaly_score_m, + ) + + results = np.array(results)[good_clients_idx].tolist() + log(INFO, "Good clients: %s", good_clients_idx) + + log(INFO, "Applying aggregate_fn") + # Convert results + weights_results = [ + (parameters_to_ndarrays(fit_res.parameters), fit_res.num_examples) + for _, fit_res in results + ] + + # Check that self.aggregate_fn has num_malicious parameter + if "num_malicious" in self.aggregate_fn.__code__.co_varnames: + # Count the number of malicious clients in + # good_clients_idx by checking FitRes + clients_state = { + str(fit_res.metrics["cid"]): fit_res.metrics["malicious"] + for _, fit_res in results + } + num_malicious = sum([clients_state[str(cid)] for cid in good_clients_idx]) + log( + INFO, + "Number of malicious clients in good_clients_idx after filtering: %s", + num_malicious, + ) + self.aggregate_parameters["num_malicious"] = num_malicious + + if "aggregation_rule" in self.aggregate_fn.__code__.co_varnames: + module = importlib.import_module( + self.aggregate_parameters["aggregation_module_name"] + ) + function_name = self.aggregate_parameters["aggregation_name"] + self.aggregate_parameters["aggregation_rule"] = getattr( + module, function_name + ) + # Remove aggregation_module_name and aggregation_name + # from self.aggregate_parameters + aggregate_parameters = self.aggregate_parameters.copy() + del aggregate_parameters["aggregation_module_name"] + del aggregate_parameters["aggregation_name"] + try: + parameters_aggregated = ndarrays_to_parameters( + self.aggregate_fn(weights_results, **aggregate_parameters) + ) + except ValueError as err: + log(WARNING, "Error in aggregate_fn: %s", err) + parameters_aggregated = ndarrays_to_parameters( + aggregate(weights_results) + ) + else: + parameters_aggregated = ndarrays_to_parameters( + self.aggregate_fn(weights_results, **self.aggregate_parameters) + ) + + # Aggregate custom metrics if aggregation fn was provided + metrics_aggregated = {} + if self.fit_metrics_aggregation_fn: + fit_metrics = [(res.num_examples, res.metrics) for _, res in results] + metrics_aggregated = self.fit_metrics_aggregation_fn(fit_metrics) + elif server_round == 1: # Only log this warning once + log(WARNING, "No fit_metrics_aggregation_fn provided") + + # Add good_clients_idx and malicious_clients_idx to metrics_aggregated + metrics_aggregated["good_clients_idx"] = good_clients_idx + metrics_aggregated["malicious_clients_idx"] = malicious_clients_idx + + return parameters_aggregated, metrics_aggregated + + +# pylint: disable=too-many-locals, too-many-arguments, invalid-name +def mar(X, pred_step, alpha=1, beta=1, maxiter=100): + """Forecast the next tensor of params. + + Forecast the next tensor of params by using MAR algorithm. + + Code provided by Xinyu Chen at: + https://towardsdatascience.com/ matrix-autoregressive-model-for-multidimensional- + time-series-forecasting-6a4d7dce5143 + + With some modifications. + """ + m, n, T = X.shape + start = 0 + + A = np.random.randn(m, m) + B = np.random.randn(n, n) + X_norm = (X - np.min(X)) / np.max(X) + + for _ in range(maxiter): + temp0 = B.T @ B + temp1 = np.zeros((m, m)) + temp2 = np.zeros((m, m)) + identity_m = np.identity(m) + + for t in range(start, T): + temp1 += X_norm[:, :, t] @ B @ X_norm[:, :, t - 1].T + temp2 += X_norm[:, :, t - 1] @ temp0 @ X_norm[:, :, t - 1].T + + temp2 += alpha * identity_m + A = temp1 @ np.linalg.inv(temp2) + + temp0 = A.T @ A + temp1 = np.zeros((n, n)) + temp2 = np.zeros((n, n)) + identity_n = np.identity(n) + + for t in range(start, T): + temp1 += X_norm[:, :, t].T @ A @ X_norm[:, :, t - 1] + temp2 += X_norm[:, :, t - 1].T @ temp0 @ X_norm[:, :, t - 1] + + temp2 += beta * identity_n + B = temp1 @ np.linalg.inv(temp2) + + tensor = np.append(X, np.zeros((m, n, pred_step)), axis=2) + for s in range(pred_step): + tensor[:, :, T + s] = A @ tensor[:, :, T + s - 1] @ B.T + return tensor[:, :, -pred_step:] diff --git a/baselines/flanders/flanders/utils.py b/baselines/flanders/flanders/utils.py new file mode 100644 index 000000000000..619e685e51cd --- /dev/null +++ b/baselines/flanders/flanders/utils.py @@ -0,0 +1,182 @@ +"""Collection of help functions needed by the strategies.""" + +import os +from threading import Lock +from typing import Callable, Dict, List, Optional, Tuple + +import numpy as np +import torch +from flwr.common import NDArrays, Parameters, Scalar, parameters_to_ndarrays +from natsort import natsorted +from torch.utils.data import DataLoader +from torchvision import transforms +from torchvision.datasets import MNIST, FashionMNIST + +from .client import set_params +from .models import FMnistNet, MnistNet, test_fmnist, test_mnist + +lock = Lock() + + +def l2_norm(true_matrix, predicted_matrix): + """Compute the l2 norm between two matrices. + + Parameters + ---------- + true_matrix : ndarray + The true matrix. + predicted_matrix : ndarray + The predicted matrix by MAR. + + Returns + ------- + anomaly_scores : ndarray + 1-d array of anomaly scores. + """ + delta = np.subtract(true_matrix, predicted_matrix) + anomaly_scores = np.sum(delta**2, axis=-1) ** (1.0 / 2) + return anomaly_scores + + +def save_params( + parameters, cid, params_dir="clients_params", remove_last=False, rrl=False +): + """Save parameters in a file. + + Args: + - parameters (ndarray): decoded parameters to append at the end of the file + - cid (int): identifier of the client + - remove_last (bool): + if True, remove the last saved parameters and replace with "parameters" + - rrl (bool): + if True, remove the last saved parameters and replace with the ones + saved before this round. + """ + new_params = parameters + # Save parameters in clients_params/cid_params + path_file = f"{params_dir}/{cid}_params.npy" + if os.path.exists(params_dir) is False: + os.mkdir(params_dir) + if os.path.exists(path_file): + # load old parameters + old_params = np.load(path_file, allow_pickle=True) + if remove_last: + old_params = old_params[:-1] + if rrl: + new_params = old_params[-1] + # add new parameters + new_params = np.vstack((old_params, new_params)) + + # save parameters + np.save(path_file, new_params) + + +def load_all_time_series(params_dir="clients_params", window=0): + """Load all time series. + + Load all time series in order to have a tensor of shape (m,T,n) + where: + - T := time; + - m := number of clients; + - n := number of parameters. + """ + files = os.listdir(params_dir) + files = natsorted(files) + data = [] + for file in files: + data.append(np.load(os.path.join(params_dir, file), allow_pickle=True)) + + return np.array(data)[:, -window:, :] + + +def flatten_params(params): + """Transform a list of (layers-)parameters into a single vector of shape (n).""" + return np.concatenate(params, axis=None).ravel() + + +# pylint: disable=unused-argument +def evaluate_aggregated( + evaluate_fn: Optional[ + Callable[[int, NDArrays, Dict[str, Scalar]], Tuple[float, Dict[str, Scalar]]] + ], + server_round: int, + parameters: Parameters, +): + """Evaluate model parameters using an evaluation function.""" + if evaluate_fn is None: + # No evaluation function provided + return None + parameters_ndarrays = parameters_to_ndarrays(parameters) + eval_res = evaluate_fn(server_round, parameters_ndarrays, {}) + if eval_res is None: + return None + loss, metrics = eval_res + + return loss, metrics + + +# pylint: disable=unused-argument +def mnist_evaluate(server_round: int, parameters: NDArrays, config: Dict[str, Scalar]): + """Evaluate MNIST model on the test set.""" + # determine device + if torch.cuda.is_available(): + device = torch.device("cuda") + elif torch.backends.mps.is_available(): + device = torch.device("mps") + else: + device = torch.device("cpu") + + model = MnistNet() + set_params(model, parameters) + model.to(device) + + testset = MNIST("", train=False, download=True, transform=transforms.ToTensor()) + testloader = DataLoader(testset, batch_size=32, shuffle=False, num_workers=1) + loss, accuracy, auc = test_mnist(model, testloader, device=device) + + return loss, {"accuracy": accuracy, "auc": auc} + + +# pylint: disable=unused-argument +def fmnist_evaluate(server_round: int, parameters: NDArrays, config: Dict[str, Scalar]): + """Evaluate MNIST model on the test set.""" + # determine device + if torch.cuda.is_available(): + device = torch.device("cuda") + elif torch.backends.mps.is_available(): + device = torch.device("mps") + else: + device = torch.device("cpu") + + model = FMnistNet() + set_params(model, parameters) + model.to(device) + + testset = FashionMNIST( + "", train=False, download=True, transform=transforms.ToTensor() + ) + testloader = DataLoader(testset, batch_size=32, shuffle=False, num_workers=1) + loss, accuracy, auc = test_fmnist(model, testloader, device=device) + + return loss, {"accuracy": accuracy, "auc": auc} + + +def update_confusion_matrix( + confusion_matrix: Dict[str, int], + clients_states: Dict[str, bool], + malicious_clients_idx: List, + good_clients_idx: List, +): + """Update TN, FP, FN, TP of confusion matrix.""" + for client_idx, client_state in clients_states.items(): + if int(client_idx) in malicious_clients_idx: + if client_state: + confusion_matrix["TP"] += 1 + else: + confusion_matrix["FP"] += 1 + elif int(client_idx) in good_clients_idx: + if client_state: + confusion_matrix["FN"] += 1 + else: + confusion_matrix["TN"] += 1 + return confusion_matrix diff --git a/baselines/flanders/plotting/FLANDERS_results.ipynb b/baselines/flanders/plotting/FLANDERS_results.ipynb new file mode 100644 index 000000000000..4f3fdcc9b0d8 --- /dev/null +++ b/baselines/flanders/plotting/FLANDERS_results.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","metadata":{"id":"Cg37xeuu7Xy5"},"source":["# Preliminaries"]},{"cell_type":"code","execution_count":92,"metadata":{"id":"J_Dh3sGVyb2w"},"outputs":[],"source":["import pandas as pd\n","from natsort import natsorted\n","import matplotlib.pyplot as plt"]},{"cell_type":"code","execution_count":93,"metadata":{"id":"FjlCyr_B8OdT"},"outputs":[],"source":["results_dir = \"../outputs/\""]},{"cell_type":"markdown","metadata":{"id":"VX2oCpZf7Z7y"},"source":["# Prepare data"]},{"cell_type":"markdown","metadata":{"id":"P_3Z05w0wvNB"},"source":["## Utils"]},{"cell_type":"code","execution_count":94,"metadata":{},"outputs":[],"source":["def divide_results_by_dataset(results_dir, file=\"all_results.csv\"):\n"," \"\"\"Divide csv results into multiple files distinguished by dataset and if strategy is FLANDERS or not (e.g., all_results_mnist_flanders and all_results_mnist_no_flanders).\"\"\"\n"," results = pd.read_csv(results_dir + file, float_precision='round_trip')\n"," datasets = natsorted(results[\"dataset_name\"].unique())\n"," for dataset in datasets:\n"," flanders = results[(results[\"dataset_name\"] == dataset) & (results[\"strategy\"] == \"flanders\")]\n"," no_flanders = results[(results[\"dataset_name\"] == dataset) & (results[\"strategy\"] != \"flanders\")]\n"," flanders.to_csv(results_dir + \"all_results_\" + dataset + \"_flanders.csv\", index=False)\n"," no_flanders.to_csv(results_dir + \"all_results_\" + dataset + \"_no_flanders.csv\", index=False)\n"," "]},{"cell_type":"code","execution_count":95,"metadata":{"id":"fZSDCuT497HV"},"outputs":[],"source":["def print_unique_data(results_df):\n"," for col in [\"attack_fn\", \"num_malicious\", \"dataset_name\", \"strategy\", \"aggregate_fn\"]:\n"," print(f\"Unique values in {col}: {results_df[col].unique()}\")"]},{"cell_type":"code","execution_count":96,"metadata":{"id":"8GcIZNuu8q5Y"},"outputs":[],"source":["def translate_cols(df, attack_dict, dataset_dict, strategy_dict, aggregate_dict):\n"," column_names = [\"attack_fn\", \"dataset_name\", \"strategy\", \"aggregate_fn\"]\n"," for idx, d in enumerate([attack_dict, dataset_dict, strategy_dict, aggregate_dict]):\n"," df[column_names[idx]] = df[column_names[idx]].replace(d)\n"," return df"]},{"cell_type":"code","execution_count":97,"metadata":{"id":"oHcF2pl8sdOG"},"outputs":[],"source":["attack_dict = {\n"," \"gaussian\": \"GAUSS\",\n"," \"lie\": \"LIE\",\n"," \"fang\": \"OPT\",\n"," \"minmax\": \"AGR-MM\",\n"," \"adaptive\": \"MAR-ATK\"\n","}\n","\n","dataset_dict = {\n"," \"mnist\": \"MNIST\",\n"," \"fmnist\": \"FMNIST\",\n"," \"cifar\": \"CIFAR-10\",\n"," \"cifar100\": \"CIFAR-100\"\n","}\n","\n","strategy_dict = {\n"," \"flanders\": \"FLANDERS\",\n"," \"fedavg\": \"FedAvg\",\n"," \"fedmedian\": \"FedMedian\",\n"," \"trimmedmean\": \"TrimmedMean\",\n"," \"bulyan\": \"Bulyan\",\n"," \"krum\": \"MultiKrum\",\n"," \"fldetector\": \"FLDetector\"\n","}\n","\n","aggregate_dict = {\n"," \"flwr.server.strategy.aggregate.aggregate\": \"FedAvg\",\n"," \"flwr.server.strategy.aggregate.aggregate_median\": \"FedMedian\",\n"," \"flwr.server.strategy.aggregate.aggregate_trimmed_avg\": \"TrimmedMean\",\n"," \"flwr.server.strategy.aggregate.aggregate_bulyan\": \"Bulyan\",\n"," \"flwr.server.strategy.aggregate.aggregate_krum\": \"MultiKrum\"\n","}"]},{"cell_type":"code","execution_count":98,"metadata":{},"outputs":[],"source":["divide_results_by_dataset(results_dir)"]},{"cell_type":"markdown","metadata":{"id":"y0XCCkuhwydB"},"source":["## MNIST"]},{"cell_type":"markdown","metadata":{"id":"NG2-2cpnyjkY"},"source":["### Use this shortcut"]},{"cell_type":"code","execution_count":99,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":443},"executionInfo":{"elapsed":244,"status":"ok","timestamp":1716376729975,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"lY85nEb6yrXu","outputId":"5439a3bc-684f-492f-b615-53e2252cd94c"},"outputs":[{"data":{"text/html":["

\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
roundlossaccuracyaucTPTNFPFNattack_fndataset_namenum_maliciousstrategyaggregate_fn
00720.8331210.10450.5034220000GAUSSMNIST0FLANDERSFedAvg
11664.5222520.20890.5631160000GAUSSMNIST0FLANDERSFedAvg
22624.6338260.35600.6447310000GAUSSMNIST0FLANDERSFedAvg
33581.4764720.47730.710941010000GAUSSMNIST0FLANDERSFedAvg
44545.1142050.54300.746970020000GAUSSMNIST0FLANDERSFedAvg
..........................................
356546724.2817250.10250.5004790000AGR-MMMNIST80dncFedAvg
356647724.3501380.10240.5004210000AGR-MMMNIST80dncFedAvg
356748724.5352630.10250.5004790000AGR-MMMNIST80dncFedAvg
356849724.5888810.10280.5005980000AGR-MMMNIST80dncFedAvg
356950724.7838510.10280.5006020000AGR-MMMNIST80dncFedAvg
\n","

7548 rows Γ— 13 columns

\n","
"],"text/plain":[" round loss accuracy auc TP TN FP FN attack_fn \\\n","0 0 720.833121 0.1045 0.503422 0 0 0 0 GAUSS \n","1 1 664.522252 0.2089 0.563116 0 0 0 0 GAUSS \n","2 2 624.633826 0.3560 0.644731 0 0 0 0 GAUSS \n","3 3 581.476472 0.4773 0.710941 0 100 0 0 GAUSS \n","4 4 545.114205 0.5430 0.746970 0 200 0 0 GAUSS \n","... ... ... ... ... .. ... .. .. ... \n","3565 46 724.281725 0.1025 0.500479 0 0 0 0 AGR-MM \n","3566 47 724.350138 0.1024 0.500421 0 0 0 0 AGR-MM \n","3567 48 724.535263 0.1025 0.500479 0 0 0 0 AGR-MM \n","3568 49 724.588881 0.1028 0.500598 0 0 0 0 AGR-MM \n","3569 50 724.783851 0.1028 0.500602 0 0 0 0 AGR-MM \n","\n"," dataset_name num_malicious strategy aggregate_fn \n","0 MNIST 0 FLANDERS FedAvg \n","1 MNIST 0 FLANDERS FedAvg \n","2 MNIST 0 FLANDERS FedAvg \n","3 MNIST 0 FLANDERS FedAvg \n","4 MNIST 0 FLANDERS FedAvg \n","... ... ... ... ... \n","3565 MNIST 80 dnc FedAvg \n","3566 MNIST 80 dnc FedAvg \n","3567 MNIST 80 dnc FedAvg \n","3568 MNIST 80 dnc FedAvg \n","3569 MNIST 80 dnc FedAvg \n","\n","[7548 rows x 13 columns]"]},"execution_count":99,"metadata":{},"output_type":"execute_result"}],"source":["# CSV pre-processing MNIST\n","results_flanders_file = results_dir + \"all_results_mnist_flanders.csv\"\n","results_no_flanders_file = results_dir + \"all_results_mnist_no_flanders.csv\"\n","results_flanders_df = pd.read_csv(results_flanders_file)\n","results_no_flanders_df = pd.read_csv(results_no_flanders_file)\n","results_flanders_df = translate_cols(results_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)\n","results_no_flanders_df = translate_cols(results_no_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)\n","mnist_df = pd.concat([results_flanders_df, results_no_flanders_df])\n","mnist_df"]},{"cell_type":"code","execution_count":100,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9,"status":"ok","timestamp":1716115669854,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"hg3ysnqiNrms","outputId":"bae8ab71-ce7b-409a-b154-6ad42f9dfc3b"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['MNIST']\n","Unique values in strategy: ['FLANDERS' 'FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan'\n"," 'flanders.strategies.aggregate.aggregate_dnc']\n"]}],"source":["print_unique_data(mnist_df)"]},{"cell_type":"markdown","metadata":{"id":"dE_uqUeuyl6M"},"source":["### Step-by-step processing"]},{"cell_type":"code","execution_count":101,"metadata":{"id":"R9Cpe8bF8a2z"},"outputs":[],"source":["results_flanders_file = results_dir + \"all_results_mnist_flanders.csv\"\n","results_no_flanders_file = results_dir + \"all_results_mnist_no_flanders.csv\""]},{"cell_type":"code","execution_count":102,"metadata":{"id":"8nPsIraZ7nJK"},"outputs":[],"source":["results_flanders_df = pd.read_csv(results_flanders_file)\n","results_no_flanders_df = pd.read_csv(results_no_flanders_file)"]},{"cell_type":"code","execution_count":103,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3,"status":"ok","timestamp":1707513800371,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"oC_C6WVMshle","outputId":"20708ffb-24d9-4d94-fc83-6ab93c8d4ed0"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['gaussian' 'lie' 'fang' 'minmax']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['mnist']\n","Unique values in strategy: ['flanders']\n","Unique values in aggregate_fn: ['flwr.server.strategy.aggregate.aggregate'\n"," 'flwr.server.strategy.aggregate.aggregate_trimmed_avg'\n"," 'flwr.server.strategy.aggregate.aggregate_median'\n"," 'flwr.server.strategy.aggregate.aggregate_krum'\n"," 'flwr.server.strategy.aggregate.aggregate_bulyan'\n"," 'flanders.strategies.aggregate.aggregate_dnc']\n"]}],"source":["print_unique_data(results_flanders_df)"]},{"cell_type":"code","execution_count":104,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":423,"status":"ok","timestamp":1707478795736,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"2xSStl9-52cc","outputId":"391a3d7c-c4b5-486c-85a2-97d9a5ddb30d"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['gaussian' 'lie' 'fang' 'minmax']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['mnist']\n","Unique values in strategy: ['fedavg' 'trimmedmean' 'fedmedian' 'krum' 'bulyan' 'dnc']\n","Unique values in aggregate_fn: ['flwr.server.strategy.aggregate.aggregate']\n"]}],"source":["print_unique_data(results_no_flanders_df)"]},{"cell_type":"markdown","metadata":{"id":"8dEepZY28raZ"},"source":["Translate strings"]},{"cell_type":"code","execution_count":105,"metadata":{"id":"zNPGc6YJ7E_J"},"outputs":[],"source":["results_flanders_df = translate_cols(results_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)"]},{"cell_type":"code","execution_count":106,"metadata":{"id":"AQaNnF1K7TQc"},"outputs":[],"source":["results_no_flanders_df = translate_cols(results_no_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)"]},{"cell_type":"code","execution_count":107,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":393,"status":"ok","timestamp":1707478246670,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"MXYnAzh8V-9t","outputId":"8f09ab5c-cd3e-4627-9f76-5f474ec09227"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['MNIST']\n","Unique values in strategy: ['FLANDERS']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan'\n"," 'flanders.strategies.aggregate.aggregate_dnc']\n"]}],"source":["print_unique_data(results_flanders_df)"]},{"cell_type":"code","execution_count":108,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":8,"status":"ok","timestamp":1707472989224,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"7xmPF5X77VQk","outputId":"f8e8331e-cde6-4413-f795-f0fe1a9cdd19"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['MNIST']\n","Unique values in strategy: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg']\n"]}],"source":["print_unique_data(results_no_flanders_df)"]},{"cell_type":"markdown","metadata":{"id":"mjTmoV2M9YTk"},"source":["Concatenate the 2 dataframes, namely FLANDERS+f and baselines:"]},{"cell_type":"code","execution_count":109,"metadata":{"id":"apvpT9Ve8wwv"},"outputs":[],"source":["mnist_df = pd.concat([results_flanders_df, results_no_flanders_df])"]},{"cell_type":"code","execution_count":110,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":424},"executionInfo":{"elapsed":5,"status":"ok","timestamp":1707513807441,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"aZuW73BW9Iu3","outputId":"3f558906-0d55-4ccd-e64f-5b62203ae746"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
roundlossaccuracyaucTPTNFPFNattack_fndataset_namenum_maliciousstrategyaggregate_fn
00720.8331210.10450.5034220000GAUSSMNIST0FLANDERSFedAvg
11664.5222520.20890.5631160000GAUSSMNIST0FLANDERSFedAvg
22624.6338260.35600.6447310000GAUSSMNIST0FLANDERSFedAvg
33581.4764720.47730.710941010000GAUSSMNIST0FLANDERSFedAvg
44545.1142050.54300.746970020000GAUSSMNIST0FLANDERSFedAvg
..........................................
356546724.2817250.10250.5004790000AGR-MMMNIST80dncFedAvg
356647724.3501380.10240.5004210000AGR-MMMNIST80dncFedAvg
356748724.5352630.10250.5004790000AGR-MMMNIST80dncFedAvg
356849724.5888810.10280.5005980000AGR-MMMNIST80dncFedAvg
356950724.7838510.10280.5006020000AGR-MMMNIST80dncFedAvg
\n","

7548 rows Γ— 13 columns

\n","
"],"text/plain":[" round loss accuracy auc TP TN FP FN attack_fn \\\n","0 0 720.833121 0.1045 0.503422 0 0 0 0 GAUSS \n","1 1 664.522252 0.2089 0.563116 0 0 0 0 GAUSS \n","2 2 624.633826 0.3560 0.644731 0 0 0 0 GAUSS \n","3 3 581.476472 0.4773 0.710941 0 100 0 0 GAUSS \n","4 4 545.114205 0.5430 0.746970 0 200 0 0 GAUSS \n","... ... ... ... ... .. ... .. .. ... \n","3565 46 724.281725 0.1025 0.500479 0 0 0 0 AGR-MM \n","3566 47 724.350138 0.1024 0.500421 0 0 0 0 AGR-MM \n","3567 48 724.535263 0.1025 0.500479 0 0 0 0 AGR-MM \n","3568 49 724.588881 0.1028 0.500598 0 0 0 0 AGR-MM \n","3569 50 724.783851 0.1028 0.500602 0 0 0 0 AGR-MM \n","\n"," dataset_name num_malicious strategy aggregate_fn \n","0 MNIST 0 FLANDERS FedAvg \n","1 MNIST 0 FLANDERS FedAvg \n","2 MNIST 0 FLANDERS FedAvg \n","3 MNIST 0 FLANDERS FedAvg \n","4 MNIST 0 FLANDERS FedAvg \n","... ... ... ... ... \n","3565 MNIST 80 dnc FedAvg \n","3566 MNIST 80 dnc FedAvg \n","3567 MNIST 80 dnc FedAvg \n","3568 MNIST 80 dnc FedAvg \n","3569 MNIST 80 dnc FedAvg \n","\n","[7548 rows x 13 columns]"]},"execution_count":110,"metadata":{},"output_type":"execute_result"}],"source":["mnist_df"]},{"cell_type":"code","execution_count":111,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3,"status":"ok","timestamp":1707480685917,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"Ub0W-iA69LpR","outputId":"bfbcaf9e-575c-4d02-e9f0-0be7e74accb4"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['MNIST']\n","Unique values in strategy: ['FLANDERS' 'FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan'\n"," 'flanders.strategies.aggregate.aggregate_dnc']\n"]}],"source":["print_unique_data(mnist_df)"]},{"cell_type":"markdown","metadata":{"id":"E3TZ_fJuTVuU"},"source":["## Fashion MNIST"]},{"cell_type":"markdown","metadata":{"id":"45GAIKG9Tmyb"},"source":["### Use this shortcut"]},{"cell_type":"code","execution_count":112,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":443},"executionInfo":{"elapsed":327,"status":"ok","timestamp":1716376732776,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"Qju5S7VmTpB_","outputId":"fbe0aed8-164c-4343-9949-e9fa7cc7f0a7"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
roundlossaccuracyaucTPTNFPFNattack_fndataset_namenum_maliciousstrategyaggregate_fn
0023082.3338130.06310.4795000000GAUSSFMNIST0FLANDERSFedAvg
1121920.1315610.19770.5542780000GAUSSFMNIST0FLANDERSFedAvg
2217859.0960200.42100.6783330000GAUSSFMNIST0FLANDERSFedAvg
3315559.0449260.49200.717778010000GAUSSFMNIST0FLANDERSFedAvg
4414684.1937220.50010.722278020000GAUSSFMNIST0FLANDERSFedAvg
..........................................
35654623279.5649070.10000.5000000000AGR-MMFMNIST80dncFedAvg
35664723290.9804420.10000.5000000000AGR-MMFMNIST80dncFedAvg
35674823302.2510220.10000.5000000000AGR-MMFMNIST80dncFedAvg
35684923312.5125960.10000.5000000000AGR-MMFMNIST80dncFedAvg
35695023326.1161770.10000.5000000000AGR-MMFMNIST80dncFedAvg
\n","

6884 rows Γ— 13 columns

\n","
"],"text/plain":[" round loss accuracy auc TP TN FP FN attack_fn \\\n","0 0 23082.333813 0.0631 0.479500 0 0 0 0 GAUSS \n","1 1 21920.131561 0.1977 0.554278 0 0 0 0 GAUSS \n","2 2 17859.096020 0.4210 0.678333 0 0 0 0 GAUSS \n","3 3 15559.044926 0.4920 0.717778 0 100 0 0 GAUSS \n","4 4 14684.193722 0.5001 0.722278 0 200 0 0 GAUSS \n","... ... ... ... ... .. ... .. .. ... \n","3565 46 23279.564907 0.1000 0.500000 0 0 0 0 AGR-MM \n","3566 47 23290.980442 0.1000 0.500000 0 0 0 0 AGR-MM \n","3567 48 23302.251022 0.1000 0.500000 0 0 0 0 AGR-MM \n","3568 49 23312.512596 0.1000 0.500000 0 0 0 0 AGR-MM \n","3569 50 23326.116177 0.1000 0.500000 0 0 0 0 AGR-MM \n","\n"," dataset_name num_malicious strategy aggregate_fn \n","0 FMNIST 0 FLANDERS FedAvg \n","1 FMNIST 0 FLANDERS FedAvg \n","2 FMNIST 0 FLANDERS FedAvg \n","3 FMNIST 0 FLANDERS FedAvg \n","4 FMNIST 0 FLANDERS FedAvg \n","... ... ... ... ... \n","3565 FMNIST 80 dnc FedAvg \n","3566 FMNIST 80 dnc FedAvg \n","3567 FMNIST 80 dnc FedAvg \n","3568 FMNIST 80 dnc FedAvg \n","3569 FMNIST 80 dnc FedAvg \n","\n","[6884 rows x 13 columns]"]},"execution_count":112,"metadata":{},"output_type":"execute_result"}],"source":["# CSV pre-processing FMNIST\n","results_flanders_file = results_dir + \"all_results_fmnist_flanders.csv\"\n","results_no_flanders_file = results_dir + \"all_results_fmnist_no_flanders.csv\"\n","results_flanders_df = pd.read_csv(results_flanders_file)\n","results_no_flanders_df = pd.read_csv(results_no_flanders_file)\n","results_flanders_df = translate_cols(results_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)\n","results_no_flanders_df = translate_cols(results_no_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)\n","fmnist_df = pd.concat([results_flanders_df, results_no_flanders_df])\n","fmnist_df"]},{"cell_type":"code","execution_count":113,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":1,"status":"ok","timestamp":1716047458204,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"08YXqMTZNpN6","outputId":"9de6cdc8-241e-4867-cd53-645434b99ce2"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['FMNIST']\n","Unique values in strategy: ['FLANDERS' 'FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan']\n"]}],"source":["print_unique_data(fmnist_df)"]},{"cell_type":"markdown","metadata":{"id":"9vVX6wsxT-rc"},"source":["### Step-by-step processing"]},{"cell_type":"code","execution_count":114,"metadata":{"id":"j0ZnLmVnUBT3"},"outputs":[],"source":["results_flanders_file = results_dir + \"all_results_fmnist_flanders.csv\"\n","results_no_flanders_file = results_dir + \"all_results_fmnist_no_flanders.csv\""]},{"cell_type":"code","execution_count":115,"metadata":{"id":"qsYaQiAWUBOw"},"outputs":[],"source":["results_flanders_df = pd.read_csv(results_flanders_file)\n","results_no_flanders_df = pd.read_csv(results_no_flanders_file)"]},{"cell_type":"code","execution_count":116,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":250,"status":"ok","timestamp":1709217712591,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"m1VKWq_jUHyY","outputId":"5bd5b442-4ab4-473d-bc60-b6f5fdc6320d"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['gaussian' 'lie' 'fang' 'minmax']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['fmnist']\n","Unique values in strategy: ['flanders']\n","Unique values in aggregate_fn: ['flwr.server.strategy.aggregate.aggregate'\n"," 'flwr.server.strategy.aggregate.aggregate_trimmed_avg'\n"," 'flwr.server.strategy.aggregate.aggregate_median'\n"," 'flwr.server.strategy.aggregate.aggregate_krum'\n"," 'flwr.server.strategy.aggregate.aggregate_bulyan']\n"]}],"source":["print_unique_data(results_flanders_df)"]},{"cell_type":"code","execution_count":117,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":4,"status":"ok","timestamp":1709217720407,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"IxPT9D6DUJN3","outputId":"ded1f8b2-baf5-437b-abe3-25bad7a3c4ad"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['gaussian' 'lie' 'fang' 'minmax']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['fmnist']\n","Unique values in strategy: ['fedavg' 'trimmedmean' 'fedmedian' 'krum' 'bulyan' 'dnc']\n","Unique values in aggregate_fn: ['flwr.server.strategy.aggregate.aggregate']\n"]}],"source":["print_unique_data(results_no_flanders_df)"]},{"cell_type":"markdown","metadata":{"id":"X8k98LNrUaLp"},"source":["Translate strings"]},{"cell_type":"code","execution_count":118,"metadata":{"id":"zHNwpvZMUaLq"},"outputs":[],"source":["results_flanders_df = translate_cols(results_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)"]},{"cell_type":"code","execution_count":119,"metadata":{"id":"9zMOOjCiUaLr"},"outputs":[],"source":["results_no_flanders_df = translate_cols(results_no_flanders_df, attack_dict ,dataset_dict, strategy_dict, aggregate_dict)"]},{"cell_type":"code","execution_count":120,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":4,"status":"ok","timestamp":1709217802421,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"tygeoDz6UaLr","outputId":"2441a31a-ead0-4adf-8b95-73d75c6b3739"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['FMNIST']\n","Unique values in strategy: ['FLANDERS']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan']\n"]}],"source":["print_unique_data(results_flanders_df)"]},{"cell_type":"code","execution_count":121,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3,"status":"ok","timestamp":1709217803343,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"b8Xo1VseUaLr","outputId":"1fbb0a64-0695-4a89-eec1-e0a18ba54c40"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['FMNIST']\n","Unique values in strategy: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg']\n"]}],"source":["print_unique_data(results_no_flanders_df)"]},{"cell_type":"markdown","metadata":{"id":"zkmqmUTzUaLr"},"source":["Concatenate the 2 dataframes, namely FLANDERS+f and baselines:"]},{"cell_type":"code","execution_count":122,"metadata":{"id":"m-wRVa9eUaLr"},"outputs":[],"source":["fmnist_df = pd.concat([results_flanders_df, results_no_flanders_df])"]},{"cell_type":"code","execution_count":123,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":423},"executionInfo":{"elapsed":4,"status":"ok","timestamp":1709217813677,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"IhpjH5n1UaLs","outputId":"73f9a359-9f74-4e5e-b518-25af272b2207"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
roundlossaccuracyaucTPTNFPFNattack_fndataset_namenum_maliciousstrategyaggregate_fn
0023082.3338130.06310.4795000000GAUSSFMNIST0FLANDERSFedAvg
1121920.1315610.19770.5542780000GAUSSFMNIST0FLANDERSFedAvg
2217859.0960200.42100.6783330000GAUSSFMNIST0FLANDERSFedAvg
3315559.0449260.49200.717778010000GAUSSFMNIST0FLANDERSFedAvg
4414684.1937220.50010.722278020000GAUSSFMNIST0FLANDERSFedAvg
..........................................
35654623279.5649070.10000.5000000000AGR-MMFMNIST80dncFedAvg
35664723290.9804420.10000.5000000000AGR-MMFMNIST80dncFedAvg
35674823302.2510220.10000.5000000000AGR-MMFMNIST80dncFedAvg
35684923312.5125960.10000.5000000000AGR-MMFMNIST80dncFedAvg
35695023326.1161770.10000.5000000000AGR-MMFMNIST80dncFedAvg
\n","

6884 rows Γ— 13 columns

\n","
"],"text/plain":[" round loss accuracy auc TP TN FP FN attack_fn \\\n","0 0 23082.333813 0.0631 0.479500 0 0 0 0 GAUSS \n","1 1 21920.131561 0.1977 0.554278 0 0 0 0 GAUSS \n","2 2 17859.096020 0.4210 0.678333 0 0 0 0 GAUSS \n","3 3 15559.044926 0.4920 0.717778 0 100 0 0 GAUSS \n","4 4 14684.193722 0.5001 0.722278 0 200 0 0 GAUSS \n","... ... ... ... ... .. ... .. .. ... \n","3565 46 23279.564907 0.1000 0.500000 0 0 0 0 AGR-MM \n","3566 47 23290.980442 0.1000 0.500000 0 0 0 0 AGR-MM \n","3567 48 23302.251022 0.1000 0.500000 0 0 0 0 AGR-MM \n","3568 49 23312.512596 0.1000 0.500000 0 0 0 0 AGR-MM \n","3569 50 23326.116177 0.1000 0.500000 0 0 0 0 AGR-MM \n","\n"," dataset_name num_malicious strategy aggregate_fn \n","0 FMNIST 0 FLANDERS FedAvg \n","1 FMNIST 0 FLANDERS FedAvg \n","2 FMNIST 0 FLANDERS FedAvg \n","3 FMNIST 0 FLANDERS FedAvg \n","4 FMNIST 0 FLANDERS FedAvg \n","... ... ... ... ... \n","3565 FMNIST 80 dnc FedAvg \n","3566 FMNIST 80 dnc FedAvg \n","3567 FMNIST 80 dnc FedAvg \n","3568 FMNIST 80 dnc FedAvg \n","3569 FMNIST 80 dnc FedAvg \n","\n","[6884 rows x 13 columns]"]},"execution_count":123,"metadata":{},"output_type":"execute_result"}],"source":["fmnist_df"]},{"cell_type":"code","execution_count":124,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":6,"status":"ok","timestamp":1709217818750,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-60},"id":"XYwnEFa2UaLs","outputId":"44dcb4de-f892-4555-b5d8-8cfcacd37137"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['FMNIST']\n","Unique values in strategy: ['FLANDERS' 'FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan']\n"]}],"source":["print_unique_data(fmnist_df)"]},{"cell_type":"markdown","metadata":{"id":"1TUxrAF6w6cY"},"source":["## Unify datasets"]},{"cell_type":"code","execution_count":125,"metadata":{"id":"R2wOP2Eex7X2"},"outputs":[],"source":["all_datasets_df = pd.concat([mnist_df, fmnist_df])"]},{"cell_type":"code","execution_count":126,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":443},"executionInfo":{"elapsed":428,"status":"ok","timestamp":1716376740426,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"jwDN17ygyFK7","outputId":"c3db739f-98b7-4070-f826-4344553e9ab7"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
roundlossaccuracyaucTPTNFPFNattack_fndataset_namenum_maliciousstrategyaggregate_fn
00720.8331210.10450.5034220000GAUSSMNIST0FLANDERSFedAvg
11664.5222520.20890.5631160000GAUSSMNIST0FLANDERSFedAvg
22624.6338260.35600.6447310000GAUSSMNIST0FLANDERSFedAvg
33581.4764720.47730.710941010000GAUSSMNIST0FLANDERSFedAvg
44545.1142050.54300.746970020000GAUSSMNIST0FLANDERSFedAvg
..........................................
35654623279.5649070.10000.5000000000AGR-MMFMNIST80dncFedAvg
35664723290.9804420.10000.5000000000AGR-MMFMNIST80dncFedAvg
35674823302.2510220.10000.5000000000AGR-MMFMNIST80dncFedAvg
35684923312.5125960.10000.5000000000AGR-MMFMNIST80dncFedAvg
35695023326.1161770.10000.5000000000AGR-MMFMNIST80dncFedAvg
\n","

14432 rows Γ— 13 columns

\n","
"],"text/plain":[" round loss accuracy auc TP TN FP FN attack_fn \\\n","0 0 720.833121 0.1045 0.503422 0 0 0 0 GAUSS \n","1 1 664.522252 0.2089 0.563116 0 0 0 0 GAUSS \n","2 2 624.633826 0.3560 0.644731 0 0 0 0 GAUSS \n","3 3 581.476472 0.4773 0.710941 0 100 0 0 GAUSS \n","4 4 545.114205 0.5430 0.746970 0 200 0 0 GAUSS \n","... ... ... ... ... .. ... .. .. ... \n","3565 46 23279.564907 0.1000 0.500000 0 0 0 0 AGR-MM \n","3566 47 23290.980442 0.1000 0.500000 0 0 0 0 AGR-MM \n","3567 48 23302.251022 0.1000 0.500000 0 0 0 0 AGR-MM \n","3568 49 23312.512596 0.1000 0.500000 0 0 0 0 AGR-MM \n","3569 50 23326.116177 0.1000 0.500000 0 0 0 0 AGR-MM \n","\n"," dataset_name num_malicious strategy aggregate_fn \n","0 MNIST 0 FLANDERS FedAvg \n","1 MNIST 0 FLANDERS FedAvg \n","2 MNIST 0 FLANDERS FedAvg \n","3 MNIST 0 FLANDERS FedAvg \n","4 MNIST 0 FLANDERS FedAvg \n","... ... ... ... ... \n","3565 FMNIST 80 dnc FedAvg \n","3566 FMNIST 80 dnc FedAvg \n","3567 FMNIST 80 dnc FedAvg \n","3568 FMNIST 80 dnc FedAvg \n","3569 FMNIST 80 dnc FedAvg \n","\n","[14432 rows x 13 columns]"]},"execution_count":126,"metadata":{},"output_type":"execute_result"}],"source":["all_datasets_df"]},{"cell_type":"code","execution_count":127,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":2,"status":"ok","timestamp":1716376741411,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"VWrdDPtPyHHA","outputId":"1ef8b853-63f8-4eac-a785-578684dbf0a6"},"outputs":[{"name":"stdout","output_type":"stream","text":["Unique values in attack_fn: ['GAUSS' 'LIE' 'OPT' 'AGR-MM']\n","Unique values in num_malicious: [ 0 20 60 80]\n","Unique values in dataset_name: ['MNIST' 'FMNIST']\n","Unique values in strategy: ['FLANDERS' 'FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan' 'dnc']\n","Unique values in aggregate_fn: ['FedAvg' 'TrimmedMean' 'FedMedian' 'MultiKrum' 'Bulyan'\n"," 'flanders.strategies.aggregate.aggregate_dnc']\n"]}],"source":["print_unique_data(all_datasets_df)"]},{"cell_type":"markdown","metadata":{"id":"OlES57TEn2Ng"},"source":["# Tables\n"]},{"cell_type":"markdown","metadata":{"id":"hcHkTXfGbapg"},"source":["## Accuracy"]},{"cell_type":"markdown","metadata":{"id":"7F1YDs12sZbE"},"source":["### Best with improvment w.r.t. baseline"]},{"cell_type":"code","execution_count":128,"metadata":{"id":"GuQM8bzXnIGx"},"outputs":[],"source":["def accuracy_table(input_df, b):\n"," # Define strategies and attacks\n"," strategies = ['FedAvg', 'FLANDERS + FedAvg', 'FedMedian', 'FLANDERS + FedMedian', 'TrimmedMean', 'FLANDERS + TrimmedMean', 'MultiKrum', 'FLANDERS + MultiKrum', 'Bulyan', 'FLANDERS + Bulyan']\n"," attacks = ['GAUSS', 'LIE', 'OPT', 'AGR-MM']\n"," dataset_names = [\"MNIST\", \"FMNIST\"]\n","\n"," # Create MultiIndex for the columns\n"," columns = pd.MultiIndex.from_product([dataset_names, attacks], names=['Dataset', 'Attack'])\n","\n"," # Create an empty DataFrame with the defined columns and strategies\n"," df = pd.DataFrame(index=strategies, columns=columns)\n","\n"," filtered_df = input_df[(input_df['num_malicious'] == b) & (input_df['round'] >= 3)]\n"," baseline_df = filtered_df[filtered_df['strategy'] != 'FLANDERS']\n"," flanders_df = filtered_df[filtered_df['strategy'] == 'FLANDERS']\n","\n"," # Populate the DataFrame\n"," for strategy in ['FedAvg', 'TrimmedMean', 'FedMedian', 'MultiKrum', 'Bulyan']:\n"," for dataset in dataset_names:\n"," for attack in attacks:\n"," df.loc[strategy, (dataset, attack)] = round(baseline_df[(baseline_df['strategy']==strategy) & (baseline_df['attack_fn']==attack) & (baseline_df['dataset_name']==dataset)]['accuracy'].max(), 2)\n"," df.loc[f\"FLANDERS + {strategy}\", (dataset, attack)] = round(flanders_df[(flanders_df['aggregate_fn']==strategy) & (flanders_df['attack_fn']==attack) & (flanders_df['dataset_name']==dataset)]['accuracy'].max(), 2)\n","\n"," return df\n"]},{"cell_type":"code","execution_count":129,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":457},"executionInfo":{"elapsed":1243,"status":"ok","timestamp":1715943873268,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"PruXJg2vA87b","outputId":"98d83851-626b-4877-fd09-8f2f01200c65"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
DatasetMNISTFMNIST
AttackGAUSSGAUSS
FedAvg0.860.68
FLANDERS + FedAvg0.840.64
FedMedian0.830.71
FLANDERS + FedMedian0.760.73
TrimmedMean0.850.69
FLANDERS + TrimmedMean0.780.7
MultiKrum0.680.66
FLANDERS + MultiKrum0.740.73
Bulyan0.860.62
FLANDERS + Bulyan0.870.65
\n","
"],"text/plain":["Dataset MNIST FMNIST\n","Attack GAUSS GAUSS\n","FedAvg 0.86 0.68\n","FLANDERS + FedAvg 0.84 0.64\n","FedMedian 0.83 0.71\n","FLANDERS + FedMedian 0.76 0.73\n","TrimmedMean 0.85 0.69\n","FLANDERS + TrimmedMean 0.78 0.7\n","MultiKrum 0.68 0.66\n","FLANDERS + MultiKrum 0.74 0.73\n","Bulyan 0.86 0.62\n","FLANDERS + Bulyan 0.87 0.65"]},"execution_count":129,"metadata":{},"output_type":"execute_result"}],"source":["# Table 19\n","acc_0 = accuracy_table(all_datasets_df, 0).dropna(axis=1, how='all')\n","acc_0"]},{"cell_type":"code","execution_count":130,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":457},"executionInfo":{"elapsed":858,"status":"ok","timestamp":1715944158330,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"8bV7hABWbyMS","outputId":"6d24f87d-da15-40b2-8880-3b6dcefda1fd"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
DatasetMNISTFMNIST
AttackGAUSSLIEOPTAGR-MMGAUSSLIEOPTAGR-MM
FedAvg0.20.170.670.450.250.170.570.11
FLANDERS + FedAvg0.880.870.480.880.660.670.570.64
FedMedian0.80.660.790.590.660.650.670.6
FLANDERS + FedMedian0.850.850.660.830.710.690.630.73
TrimmedMean0.860.520.730.610.690.540.620.58
FLANDERS + TrimmedMean0.810.850.780.830.690.70.630.73
MultiKrum0.780.770.810.820.740.650.70.67
FLANDERS + MultiKrum0.820.860.840.820.730.70.730.71
Bulyan0.820.840.840.830.710.720.690.76
FLANDERS + Bulyan0.90.840.790.850.650.650.660.65
\n","
"],"text/plain":["Dataset MNIST FMNIST \n","Attack GAUSS LIE OPT AGR-MM GAUSS LIE OPT AGR-MM\n","FedAvg 0.2 0.17 0.67 0.45 0.25 0.17 0.57 0.11\n","FLANDERS + FedAvg 0.88 0.87 0.48 0.88 0.66 0.67 0.57 0.64\n","FedMedian 0.8 0.66 0.79 0.59 0.66 0.65 0.67 0.6\n","FLANDERS + FedMedian 0.85 0.85 0.66 0.83 0.71 0.69 0.63 0.73\n","TrimmedMean 0.86 0.52 0.73 0.61 0.69 0.54 0.62 0.58\n","FLANDERS + TrimmedMean 0.81 0.85 0.78 0.83 0.69 0.7 0.63 0.73\n","MultiKrum 0.78 0.77 0.81 0.82 0.74 0.65 0.7 0.67\n","FLANDERS + MultiKrum 0.82 0.86 0.84 0.82 0.73 0.7 0.73 0.71\n","Bulyan 0.82 0.84 0.84 0.83 0.71 0.72 0.69 0.76\n","FLANDERS + Bulyan 0.9 0.84 0.79 0.85 0.65 0.65 0.66 0.65"]},"execution_count":130,"metadata":{},"output_type":"execute_result"}],"source":["# Table 15\n","acc_20 = accuracy_table(all_datasets_df, 20)\n","acc_20"]},{"cell_type":"markdown","metadata":{},"source":["Bulyan is NaN because it cannot work when the number of malicious clients is > 25%"]},{"cell_type":"code","execution_count":131,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":477},"executionInfo":{"elapsed":1126,"status":"ok","timestamp":1716115710006,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"xSvgEwLoPmh3","outputId":"1c65e66e-7374-46f0-a2a5-0964bc40e49a"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
DatasetMNISTFMNIST
AttackGAUSSLIEOPTAGR-MMGAUSSLIEOPTAGR-MM
FedAvg0.190.150.20.160.280.10.190.1
FLANDERS + FedAvg0.760.880.850.850.690.670.710.67
FedMedian0.80.190.160.290.650.10.10.1
FLANDERS + FedMedian0.80.860.830.860.710.690.710.71
TrimmedMean0.250.20.330.10.330.10.170.1
FLANDERS + TrimmedMean0.780.870.840.830.70.710.730.74
MultiKrum0.790.140.220.150.710.10.120.1
FLANDERS + MultiKrum0.880.880.860.780.720.710.730.69
BulyanNaNNaNNaNNaNNaNNaNNaNNaN
FLANDERS + Bulyan0.890.870.90.850.680.640.60.69
\n","
"],"text/plain":["Dataset MNIST FMNIST \n","Attack GAUSS LIE OPT AGR-MM GAUSS LIE OPT AGR-MM\n","FedAvg 0.19 0.15 0.2 0.16 0.28 0.1 0.19 0.1\n","FLANDERS + FedAvg 0.76 0.88 0.85 0.85 0.69 0.67 0.71 0.67\n","FedMedian 0.8 0.19 0.16 0.29 0.65 0.1 0.1 0.1\n","FLANDERS + FedMedian 0.8 0.86 0.83 0.86 0.71 0.69 0.71 0.71\n","TrimmedMean 0.25 0.2 0.33 0.1 0.33 0.1 0.17 0.1\n","FLANDERS + TrimmedMean 0.78 0.87 0.84 0.83 0.7 0.71 0.73 0.74\n","MultiKrum 0.79 0.14 0.22 0.15 0.71 0.1 0.12 0.1\n","FLANDERS + MultiKrum 0.88 0.88 0.86 0.78 0.72 0.71 0.73 0.69\n","Bulyan NaN NaN NaN NaN NaN NaN NaN NaN\n","FLANDERS + Bulyan 0.89 0.87 0.9 0.85 0.68 0.64 0.6 0.69"]},"execution_count":131,"metadata":{},"output_type":"execute_result"}],"source":["# Table 17\n","acc_60 = accuracy_table(all_datasets_df, 60)\n","acc_60"]},{"cell_type":"code","execution_count":132,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":477},"executionInfo":{"elapsed":1188,"status":"ok","timestamp":1716050662469,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"dM_AMm_jcCye","outputId":"90533ef1-500f-40a3-f565-2c2ca1d68aaa"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
DatasetMNISTFMNIST
AttackGAUSSLIEOPTAGR-MMGAUSSLIEOPTAGR-MM
FedAvg0.210.160.310.130.240.10.180.1
FLANDERS + FedAvg0.850.860.880.850.690.70.690.66
FedMedian0.340.170.140.090.30.10.140.1
FLANDERS + FedMedian0.870.840.80.80.730.740.720.72
TrimmedMean0.170.150.210.140.210.10.120.1
FLANDERS + TrimmedMean0.810.850.810.820.740.730.70.69
MultiKrum0.820.210.320.110.720.10.150.1
FLANDERS + MultiKrum0.870.830.870.850.680.730.720.7
BulyanNaNNaNNaNNaNNaNNaNNaNNaN
FLANDERS + Bulyan0.840.840.830.80.690.720.690.68
\n","
"],"text/plain":["Dataset MNIST FMNIST \n","Attack GAUSS LIE OPT AGR-MM GAUSS LIE OPT AGR-MM\n","FedAvg 0.21 0.16 0.31 0.13 0.24 0.1 0.18 0.1\n","FLANDERS + FedAvg 0.85 0.86 0.88 0.85 0.69 0.7 0.69 0.66\n","FedMedian 0.34 0.17 0.14 0.09 0.3 0.1 0.14 0.1\n","FLANDERS + FedMedian 0.87 0.84 0.8 0.8 0.73 0.74 0.72 0.72\n","TrimmedMean 0.17 0.15 0.21 0.14 0.21 0.1 0.12 0.1\n","FLANDERS + TrimmedMean 0.81 0.85 0.81 0.82 0.74 0.73 0.7 0.69\n","MultiKrum 0.82 0.21 0.32 0.11 0.72 0.1 0.15 0.1\n","FLANDERS + MultiKrum 0.87 0.83 0.87 0.85 0.68 0.73 0.72 0.7\n","Bulyan NaN NaN NaN NaN NaN NaN NaN NaN\n","FLANDERS + Bulyan 0.84 0.84 0.83 0.8 0.69 0.72 0.69 0.68"]},"execution_count":132,"metadata":{},"output_type":"execute_result"}],"source":["# Table 3\n","acc_80 = accuracy_table(all_datasets_df, 80)\n","acc_80"]},{"cell_type":"markdown","metadata":{"id":"CZX8c37MsgFL"},"source":["### Best w.r.t. number of attackers"]},{"cell_type":"code","execution_count":133,"metadata":{"id":"xgIKM1obsmd2"},"outputs":[],"source":["def accuracy_table_attackers(input_df, aggregate_fn):\n"," # Define strategies and attacks\n"," attacks = ['GAUSS', 'LIE', 'OPT', 'AGR-MM']\n"," dataset_names = [\"MNIST\", \"FMNIST\"]\n"," num_malicious = [0, 20, 60, 80]\n","\n"," # Create MultiIndex for the columns\n"," columns = pd.MultiIndex.from_product([dataset_names, num_malicious], names=['Dataset', '# Malicious'])\n","\n"," #######\n"," #columns = pd.MultiIndex.from_product([['MNIST', 'CIFAR-10'], ['GAUSS', 'LIE', 'OPT', 'AGR-MM'], ['LAST', 'BEST']])\n"," #######\n","\n","\n"," # Create an empty DataFrame with the defined columns and strategies\n"," df = pd.DataFrame(index=attacks, columns=columns)\n","\n"," filtered_df = input_df[(input_df['aggregate_fn'] == aggregate_fn) & (input_df['round'] >= 3)]\n"," baseline_df = filtered_df[filtered_df['strategy'] != 'FLANDERS']\n"," flanders_df = filtered_df[filtered_df['strategy'] == 'FLANDERS']\n","\n"," # Populate the DataFrame\n"," for dataset in dataset_names:\n"," for attack in attacks:\n"," for b in num_malicious:\n"," if b == 0:\n"," df.loc[attack, (dataset, b)] = round(flanders_df[(flanders_df['num_malicious']==b) & (flanders_df['attack_fn']=='GAUSS') & (flanders_df['dataset_name']==dataset)]['accuracy'].max(), 2)\n"," else:\n"," df.loc[attack, (dataset, b)] = round(flanders_df[(flanders_df['num_malicious']==b) & (flanders_df['attack_fn']==attack) & (flanders_df['dataset_name']==dataset)]['accuracy'].max(), 2)\n","\n"," return df"]},{"cell_type":"code","execution_count":134,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":206},"executionInfo":{"elapsed":305,"status":"ok","timestamp":1715954054792,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"0n9vhQiQuxk_","outputId":"500dfb31-f525-4289-f337-2c945bf50cd2"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
DatasetMNISTFMNIST
# Malicious02060800206080
GAUSS0.740.820.880.870.730.730.720.68
LIE0.740.860.880.830.730.70.710.73
OPT0.740.840.860.870.730.730.730.72
AGR-MM0.740.820.780.850.730.710.690.7
\n","
"],"text/plain":["Dataset MNIST FMNIST \n","# Malicious 0 20 60 80 0 20 60 80\n","GAUSS 0.74 0.82 0.88 0.87 0.73 0.73 0.72 0.68\n","LIE 0.74 0.86 0.88 0.83 0.73 0.7 0.71 0.73\n","OPT 0.74 0.84 0.86 0.87 0.73 0.73 0.73 0.72\n","AGR-MM 0.74 0.82 0.78 0.85 0.73 0.71 0.69 0.7"]},"execution_count":134,"metadata":{},"output_type":"execute_result"}],"source":["# Table 20\n","acc_att = accuracy_table_attackers(all_datasets_df, 'MultiKrum')\n","acc_att"]},{"cell_type":"markdown","metadata":{"id":"g6yDorrubUw1"},"source":["## Precision and Recall"]},{"cell_type":"code","execution_count":135,"metadata":{"id":"y0PlDlG0bKek"},"outputs":[],"source":["def pr_table(input_df, b):\n"," strategies = ['FLANDERS']\n"," attacks = ['GAUSS', 'LIE', 'OPT', 'AGR-MM']\n"," dataset_names = [\"MNIST\", \"FMNIST\"]\n","\n"," # Create MultiIndex for the columns\n"," columns = pd.MultiIndex.from_product([strategies, attacks, ['P', 'R']], names=['Strategy', 'Attack', 'P/R'])\n","\n"," # Create an empty DataFrame with the defined columns and strategies\n"," df = pd.DataFrame(index=dataset_names, columns=columns)\n","\n"," filtered_df = input_df[(input_df['num_malicious'] == b) & (input_df['round'] == 50) & (input_df['aggregate_fn']=='FedAvg')]\n"," flanders_df = filtered_df[filtered_df['strategy'] == 'FLANDERS']\n"," strat_dfs = [flanders_df]\n","\n"," # Populate the DataFrame\n"," for dataset in dataset_names:\n"," for attack in attacks:\n"," for idx, strategy in enumerate(strategies):\n"," tp = strat_dfs[idx][(strat_dfs[idx]['attack_fn']==attack) & (strat_dfs[idx]['dataset_name']==dataset)]['TP'].iloc[0]\n"," fp = strat_dfs[idx][(strat_dfs[idx]['attack_fn']==attack) & (strat_dfs[idx]['dataset_name']==dataset)]['FP'].iloc[0]\n"," fn = strat_dfs[idx][(strat_dfs[idx]['attack_fn']==attack) & (strat_dfs[idx]['dataset_name']==dataset)]['FN'].iloc[0]\n"," df.loc[dataset, (strategy, attack, 'P')] = round(tp / (tp+fp), 2)\n"," df.loc[dataset, (strategy, attack, 'R')] = round(tp / (tp+fn), 2)\n","\n"," return df"]},{"cell_type":"code","execution_count":136,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":257},"executionInfo":{"elapsed":248,"status":"ok","timestamp":1716367268457,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"rpQyziNRh3dn","outputId":"26e2c0f9-144d-4cad-d13f-1e2351aa2081"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
StrategyFLANDERS
AttackGAUSSLIEOPTAGR-MM
P/RPRPRPRPR
MNIST1.01.01.01.00.150.151.01.0
FMNIST1.01.01.01.00.160.161.01.0
\n","
"],"text/plain":["Strategy FLANDERS \n","Attack GAUSS LIE OPT AGR-MM \n","P/R P R P R P R P R\n","MNIST 1.0 1.0 1.0 1.0 0.15 0.15 1.0 1.0\n","FMNIST 1.0 1.0 1.0 1.0 0.16 0.16 1.0 1.0"]},"execution_count":136,"metadata":{},"output_type":"execute_result"}],"source":["# Table 1\n","pr_20 = pr_table(all_datasets_df, 20)\n","pr_20"]},{"cell_type":"code","execution_count":137,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":257},"executionInfo":{"elapsed":327,"status":"ok","timestamp":1716367273542,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"ccW0Ups3iMvZ","outputId":"ba945e17-1bbe-414f-9aa0-b03fb0fe107f"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
StrategyFLANDERS
AttackGAUSSLIEOPTAGR-MM
P/RPRPRPRPR
MNIST1.01.01.01.01.01.01.01.0
FMNIST1.01.01.01.01.01.01.01.0
\n","
"],"text/plain":["Strategy FLANDERS \n","Attack GAUSS LIE OPT AGR-MM \n","P/R P R P R P R P R\n","MNIST 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0\n","FMNIST 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0"]},"execution_count":137,"metadata":{},"output_type":"execute_result"}],"source":["# Table 2\n","pr_60 = pr_table(all_datasets_df, 60)\n","pr_60"]},{"cell_type":"code","execution_count":138,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":257},"executionInfo":{"elapsed":308,"status":"ok","timestamp":1716376750779,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"05a0Gv5piS2v","outputId":"0cf6555a-4cc8-4aa5-f9b6-c8286afa130a"},"outputs":[{"data":{"text/html":["
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
StrategyFLANDERS
AttackGAUSSLIEOPTAGR-MM
P/RPRPRPRPR
MNIST1.01.01.01.01.01.01.01.0
FMNIST1.01.01.01.01.01.01.01.0
\n","
"],"text/plain":["Strategy FLANDERS \n","Attack GAUSS LIE OPT AGR-MM \n","P/R P R P R P R P R\n","MNIST 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0\n","FMNIST 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0"]},"execution_count":138,"metadata":{},"output_type":"execute_result"}],"source":["# Table 3\n","pr_80 = pr_table(all_datasets_df, 80)\n","pr_80"]},{"cell_type":"markdown","metadata":{"id":"bN7dTn2u0r6K"},"source":["# Plots"]},{"cell_type":"markdown","metadata":{"id":"xZ0wiadBsVUh"},"source":["## Accuracy over rounds"]},{"cell_type":"code","execution_count":139,"metadata":{"id":"LQ_uYJCtjdJS"},"outputs":[],"source":["df_mnist_acc_flanders = all_datasets_df[(all_datasets_df['strategy']=='FLANDERS') & (all_datasets_df['num_malicious']==80) & (all_datasets_df['dataset_name']=='MNIST') & (all_datasets_df['aggregate_fn']=='MultiKrum')]\n","df_mnist_acc_fedavg = all_datasets_df[(all_datasets_df['strategy']=='FedAvg') & (all_datasets_df['num_malicious']==80) & (all_datasets_df['dataset_name']=='MNIST')]\n","df_no_attack = all_datasets_df[(all_datasets_df['strategy']=='FedAvg') & (all_datasets_df['num_malicious']==0) & (all_datasets_df['dataset_name']=='MNIST')]"]},{"cell_type":"code","execution_count":140,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":408},"executionInfo":{"elapsed":651,"status":"ok","timestamp":1714668544212,"user":{"displayName":"Edoardo Gabrielli","userId":"12318890431187689267"},"user_tz":-120},"id":"NDkD_Qnd1iT7","outputId":"928cfa90-04a2-4624-9825-adb0d124aaf8"},"outputs":[{"data":{"image/png":"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S5s2bQCIjY1lyJAhhIeHc8cddxASEkJKSgrr1q1j6tSp3H///afdljNBglchhBBCCCGEqIOEhASuu+46IiIiWL58OU2aNKly/q233uKzzz5Dp6s64XXlypUcPnyYFStWcMkllzB79mxuvvnm02rLxo0bmT9/Pq+99hrPPPNMlXOffPIJ+fn5lc9fe+01vL292bhxIz4+PlXKZmVlnVY7ziSZNiyEEEIIIYQQdfD2229TUlLCtGnTqgWuAAaDgQceeIBmzZpVOR4TE0O7du0YMmQIw4YNIyYm5rTbkpCQAEC/fv2qndPr9fj7+1cp2759+2qBK0BQUNBpt+VMkeBVCCGEEEIIIepg/vz5tGjRgl69etX6GovFwm+//caECRMAmDBhAitWrCAjI+O02nJ0WnJMTAx2u/2kZTdv3syuXbtO655nmwSvQgghhBBCiLNGVVVKrfZG81BVtU6vo7CwkLS0NDp06FDtXH5+PtnZ2ZWPsrKyynPz588nPz+f6667DoArr7wSo9HIzJkz6/aGVujduzeDBg3i66+/JiwsjIkTJ/LZZ59x6NChamUfe+wxSktL6dKlC3379uXJJ59kyZIl2Gy202rDmSZrXoUQQgghhBBnTZnNQbspixu6GZX2vHwJbqZTD4sKCwsB8PDwqHZu8ODBbN++vfL5O++8w2OPPQZoI6Pdu3enRYsWAHh6ejJ69GhiYmJ46KGH6vAKNIqisHjxYt59911++uknZsyYwYwZM7j33nu55ppr+PLLLyunCQ8fPpy1a9fyxhtvsHjxYtauXcvbb79NYGAg33zzDVdccUWd23EmycirEEIIIYQQQpyio9mBi4uLq5378ssvWbp0KT/99FOV4/n5+SxcuJBBgwZx4MCByke/fv3YtGkT+/fvP602mc1mnn32Wfbu3UtaWhozZsygd+/e/PLLL9x3331Vyvbo0YPZs2eTl5fHhg0bePrppykqKmL8+PHs2bPntNpxpsjIqxBCCCGEEOKscTXq2fPyJQ3djEquRn2drvP29qZJkybHXTd6dA1sUlJSleOzZs3CYrHw3nvv8d5771W7LiYmhpdeegkAFxcXgCpTjv+ttLS0sszxNGnShOuuu45x48bRvn17fvnlF77//nsMhqohoMlkokePHvTo0YNWrVoxadIkZs2axQsvvFDzi28gErwKIYQQQgghzhpF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gkWvx8tDoWlwwMmvE0KICs899xw//vgjb7755nFHX5OTk9HpdJWB51EhISH4+PhU/ttek/z8fBYuXMh9993HgQMHKo/369eP3377jf3799OqVas6t7+srIw33niDadOmkZqaWuXf64KCgsrvExISGDduXK3qfOihhygvL2fr1q1nPXA9SvrQM6+wKJuKXEzoPTwrjyfllHCp7tg2UrqKbMPuVm1Jjoy8CiFE43Xac9Q6depEp06d6qMtVfz+++9MnToVDw8P/v77b7p27QpAdnY2Q4cOZfXq1Tz//PO8++67dar//vvvJzMzk8mTJ/P555/XZ9PrT3khZov2aXGGhzdNDSUSvApxFrz59D01ntPpqo7ivfzYnTWW/e+I3/MPTqp12fpydCru0dHX+r7/rFmzsFgsvPfee7z33nvVzsfExPDSSy/VqW7Q/q2eNm0aDz30EH369MHb2xtFUbjuuutwOp11qnPMmDHMnDmTN998kx9++OGE+62eadKHnjlFeVm4A04FdG7HgvTDWTmEKVoSr3ydD7qKNa9HR2kleBVCiMar0Wb+ef311wF46qmnKjtdgICAAD77TJvS98knn1T55L225syZQ0xMDI888gg9e/asnwbXN7sFHBY887U/KPM8fTEoKk0keBXijDObjDU+jP+Zxnmisv9dw3oqZevTc889h91u56233qp2LiIiAqfTSXx8fJXjmZmZ5OfnExERccK6Y2Ji6NChA7Nmzar2GDZsGNOnT68se6IAuaZzv/76KzfffDPvvfce48ePZ/jw4fTv37/aXrHR0dHs2rXrhG096sorr+S7775j+vTp3HvvvbW65lxzwfehQGmBFqBazXqUf31AYc3UMg1bjD5kmsLRVUwbdrFqXyV4FUKIxqtOweuiRYsYOnQoK1asqLHM8uXLGTp0KEuXLj3l+lNTU9m4cSPAcTMt9u/fn2bNmmGxWCrXa9VWdnY2kydPpnXr1rz88sun3LazxlJEuaLgn689zffwQacohAT6nfAyIYT4r+joaG644Qa+/PLLatltR40aBcCHH35Y5fj7778PULmm8XhSUlL4559/uOaaaxg/fny1x6RJkzhw4ADr168HwN1d24rkv4Hn0XPHO67X66tNS/34449xOBxVjo0bN47t27czZ86canUcb1rrTTfdxEcffcQXX3zBk08+WeNrPBOk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t9/y66+/cvfdd9OyZcsjHefRRVWxBz3YPNqTYLehXufVEkIcJfLz87nlllvo3Lkz8fHxxMfHc+yxx3LzzTezYcOGKo+55557UBSFSy65pMr9X3/9NYqi8OGHH1a5/5ZbbkFRlArb/H4/kyZNonfv3iQmJpKcnEy3bt247rrr2LhxY4WyP/30E6NGjSInJwez2UybNm0444wzePnll+vwG4gNUoc2vDJ7UfSzLiGBrcVuOkRmGra0qtgd2xeXEm15DZe5STVrS+vIuFchhIg9NcqK/H4/iqKQlZXFf//7X04++eQjHdfRzV+GXadgc2s//rlH1psTQtTOggULuOSSSzAYDFx66aX07NkTnU7Hxo0bmTNnDq+99hr5+fnk5OREj1FVlRkzZpCbm8v8+fNxuVzYbLbDjuWiiy7ik08+YezYsVx77bUEAgE2btzIggULOOmkk+jatSsAK1as4NRTTyU7O5trr72WzMxMCgsL+f7775k0aRK33nrrYcfSGKQObXheh5Z4BkwGFL2erbtLOFvZo+1Mr9htOGRKRWcsT17LJHkVQogYVqPktXv37vz0009s27aN0047je7du3PVVVdx6aWXkiZdWuufz4Vdp6NDZMxrfFpK48YjhGhSNm/ezJgxY8jJyWHJkiW0atWqwv6nn36aV199FZ2uYuebr7/+mm3btvHll19y1llnMWfOHK644orDimX16tUsWLCA//znP9x3330V9r3yyivY7fboz//5z39ISkpi9erVJCcnVyi7Z8+ew4qjMUkd2vC8Ti3xDFm0+SLs2zaiV1Q8OisWa8WW7rAldX+34dLSaPJa7JUHx0IIEWtq1G14/fr1rFq1iuuuuw6bzcaGDRu48847adOmDZdccgmfffZZdGp/UQ98TpxhHaag9qOtZYvGjUcI0aQ888wzlJWV8e6771ZKXAEMBgO33XYbbdu2rbA9Ly+PY489llNPPZXTTz+dvLy8w45l8+bNAAwcOLDSPr1eXyF527x5M926dauUuAJNumut1KENz+e0AxCONwEQ2PM7AI6EXPhbt3YS0g7oNlxGmlm7J6XlVQghYk+NkleAvn37MmXKFHbu3Mm7777LwIED8fv9zJ49m2HDhpGTk1Np7JKoI58LT0BrFA/oFNJaJjduPEKIJmXBggV07NiRAQMG1PgYn8/H//73P8aOHQvA2LFj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tav2XB06dKhQtr5kZ2dXub18Nkev1xvdtm3bNuDgcbZr144NGzbUY4RCCCGORlKHCtE0SPIqRDPx9zXqHA4HF1xwAV999RUXX3wxv/76K/Hx8Uc0hnA4fND9Op109hBCCBF7pA4VommQ/wqEaKaSkpKYNWsWqampbN26lYkTJwLQpk0bAPLz86s9dsuWLRXKAsTFxQHgcrmqPCYQCLBz5856if3Aa/99yYADHWyfEEIIUVdShwoRmyR5FaIZa9GiBQ888AAAzz33HHa7nb59+2K1WikpKWHevHmVjvF4PMycOROAU089tcK54uLiKCkpYc+ePZWO++yzzwgGg/UW+5AhQ1AUhR9++IGNGzdW2r9+/Xrp7iSEEOKIkTpUiNgjyasQzdxNN91EdnY2DoeD559/HrPZzM033wzAXXfdxdatW6NlA4EAt99+O7t27aJdu3aMGjUqus9oNDJkyBAAHnjggQrdm9avX88tt9xSr3FnZ2dzwQUXEA6HufHGG6Nr7QHs27ePm266CVVV6/WaQgghxIGkDhUitkjyKkQzZzKZePjhhwGYNGkSJSUlPPLIIwwdOpRNmzZxzDHHMHz4cMaMGUPHjh158803SUtLY/bs2dFuTuUef/xx4uLiePPNNznmmGMYPXo0J510Ev369eOUU04hJyenXmOfPHkyHTp04Ouvv6Zdu3ZcdNFFXHjhhbRv357du3czYsSIer2eEEIIcSCpQ4WILZK8CnEUuPzyyzn22GNxuVw8++yzmEwmPv30U1599VV69uzJsmXLmDt3LkajkVtvvZX169dz/PHHVzrPgAED+OabbzjzzDPZtWsXCxcuxO12M2nSJN599916jzszM5OVK1dy6623Eh8fz4IFC1i9ejVjxozh+++/JyUlpd6vKYQQQhxI6lAhYoeiSp8BIYQQQgghhBAxTlpehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEPElehRBCCCGEEELEvP8HHigTh2m+CKAAAAAASUVORK5CYII=","text/plain":["
"]},"metadata":{},"output_type":"display_data"}],"source":["# Figure 3\n","num_plots = 2\n","plt.style.use('default')\n","fig, axs = plt.subplots(1, num_plots, figsize=(11, 4))\n","\n","data = [df_mnist_acc_flanders, df_mnist_acc_fedavg]\n","\n","for i in range(num_plots):\n"," if i == 0:\n"," acc = df_no_attack[df_no_attack[\"attack_fn\"]=='GAUSS']['accuracy'].to_list()\n"," axs[i].plot(acc, label=\"No Attack\", linestyle='--', color='slategray')\n"," for attack in ['GAUSS', 'LIE', 'OPT', 'AGR-MM']:\n"," acc = data[i][data[i]['attack_fn']==attack]['accuracy'].to_list()\n"," x = [i for i in range(len(data))]\n"," axs[i].plot(acc, label=attack)\n"," axs[i].set_ylim((0,1.0))\n"," axs[i].set_xlabel('Round', fontsize=16)\n"," axs[i].set_ylabel('Accuracy', fontsize=16)\n"," axs[i].legend(prop={'size': 12})\n"," axs[i].tick_params(axis='both', which='major', labelsize=16)\n"," axs[i].tick_params(axis='both', which='minor', labelsize=16)\n","\n","plt.show()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":[]}],"metadata":{"colab":{"authorship_tag":"ABX9TyODCHCYl18UhHkKwq6LlRvG","collapsed_sections":["P_3Z05w0wvNB","dE_uqUeuyl6M","9vVX6wsxT-rc","RctMDJMZyPq2","R9VNz7Cv9RHn","6V4padUiYeac","pZ863s6JJbph","EJDtdXqLJX0H"],"provenance":[]},"kernelspec":{"display_name":"Python 3","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.9.18"}},"nbformat":4,"nbformat_minor":0} diff --git a/baselines/flanders/pyproject.toml b/baselines/flanders/pyproject.toml new file mode 100644 index 000000000000..416247f9c7bb --- /dev/null +++ b/baselines/flanders/pyproject.toml @@ -0,0 +1,151 @@ +[build-system] +requires = ["poetry-core>=1.4.0"] +build-backend = "poetry.masonry.api" + +[tool.poetry] +name = "flanders" +version = "1.0.0" +description = "FLANDERS" +license = "Apache-2.0" +authors = ["Edoardo Gabrielli "] +readme = "README.md" +homepage = "https://flower.dev" +repository = "https://github.com/adap/flower" +documentation = "https://flower.dev" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +[tool.poetry.dependencies] +python = ">=3.10, <3.12.0" +hydra-core = "1.3.2" # don't change this +flwr = {extras = ["simulation"], version = "1.6.0" } +torch = [ + { platform = "darwin", version = "2.1.1" }, + { platform = "linux", url = "https://download.pytorch.org/whl/cu118/torch-2.1.1%2Bcu118-cp310-cp310-linux_x86_64.whl" } + ] +torchvision = [ + { platform = "darwin", version = "0.16.1"}, + { platform = "linux", url = "https://download.pytorch.org/whl/cu118/torchvision-0.16.1%2Bcu118-cp310-cp310-linux_x86_64.whl" } + ] +pandas = "^2.1.3" +scikit-learn = "1.3.2" +ipykernel = "^6.27.1" +natsort = "^8.4.0" +seaborn = "^0.13.0" + +[tool.poetry.dev-dependencies] +isort = "==5.11.5" +black = "==23.1.0" +docformatter = "==1.5.1" +mypy = "==1.4.1" +pylint = "==2.8.2" +flake8 = "==3.9.2" +pytest = "==6.2.4" +pytest-watch = "==4.2.0" +ruff = "==0.0.272" +types-requests = "==2.27.7" +virtualenv = "20.21.0" + +[tool.isort] +line_length = 88 +indent = " " +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true + +[tool.black] +line-length = 88 +target-version = ["py38", "py39", "py310", "py311"] + +[tool.pytest.ini_options] +minversion = "6.2" +addopts = "-qq" +testpaths = [ + "flwr_baselines", +] + +[tool.mypy] +ignore_missing_imports = true +strict = false +plugins = "numpy.typing.mypy_plugin" + +[tool.pylint."MESSAGES CONTROL"] +disable = "bad-continuation,duplicate-code,too-few-public-methods,useless-import-alias" +good-names = "i,j,k,_,x,y,X,Y" +signature-mutators="hydra.main.main" + +[tool.pylint.typecheck] +generated-members="numpy.*, torch.*, tensorflow.*" + +[[tool.mypy.overrides]] +module = [ + "importlib.metadata.*", + "importlib_metadata.*", +] +follow_imports = "skip" +follow_imports_for_stubs = true +disallow_untyped_calls = false + +[[tool.mypy.overrides]] +module = "torch.*" +follow_imports = "skip" +follow_imports_for_stubs = true + +[tool.docformatter] +wrap-summaries = 88 +wrap-descriptions = 88 + +[tool.ruff] +target-version = "py38" +line-length = 88 +select = ["D", "E", "F", "W", "B", "ISC", "C4"] +fixable = ["D", "E", "F", "W", "B", "ISC", "C4"] +ignore = ["B024", "B027"] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "proto", +] + +[tool.ruff.pydocstyle] +convention = "numpy" diff --git a/baselines/flanders/run.sh b/baselines/flanders/run.sh new file mode 100644 index 000000000000..435c358c4ee7 --- /dev/null +++ b/baselines/flanders/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +python -m flanders.main --multirun server.num_rounds=50 dataset=mnist strategy=flanders aggregate_fn=fedavg,trimmedmean,fedmedian,krum,bulyan server.pool_size=100 server.num_malicious=0,20,60,80 server.attack_fn=gaussian,lie,fang,minmax server.warmup_rounds=2 client_resources.num_cpus=0.1 client_resources.num_gpus=0.1 + +python -m flanders.main --multirun server.num_rounds=50 dataset=mnist strategy=fedavg,trimmedmean,fedmedian,krum,bulyan server.pool_size=100 server.num_malicious=0,20,60,80 server.attack_fn=gaussian,lie,fang,minmax server.warmup_rounds=2 client_resources.num_cpus=0.1 client_resources.num_gpus=0.1 + +python -m flanders.main --multirun server.num_rounds=50 dataset=fmnist strategy=flanders aggregate_fn=fedavg,trimmedmean,fedmedian,krum,bulyan server.pool_size=100 server.num_malicious=0,20,60,80 server.attack_fn=gaussian,lie,fang,minmax server.warmup_rounds=2 client_resources.num_cpus=0.1 client_resources.num_gpus=0.1 + +python -m flanders.main --multirun server.num_rounds=50 dataset=fmnist strategy=fedavg,trimmedmean,fedmedian,krum,bulyan server.pool_size=100 server.num_malicious=0,20,60,80 server.attack_fn=gaussian,lie,fang,minmax server.warmup_rounds=2 client_resources.num_cpus=0.1 client_resources.num_gpus=0.1 \ No newline at end of file From 9c3e4af0199f7ac1c3d920f319fab754610db322 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 4 Aug 2024 18:12:24 +0100 Subject: [PATCH 018/188] fix(baselines:skip) Fix `FLANDERS` readme (#3947) --- baselines/flanders/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/baselines/flanders/README.md b/baselines/flanders/README.md index f5ab6a02d6f3..46d95211d619 100644 --- a/baselines/flanders/README.md +++ b/baselines/flanders/README.md @@ -5,6 +5,10 @@ labels: [robustness, model poisoning, anomaly detection, autoregressive model, r dataset: [MNIST, FashionMNIST] --- +# FLANDERS: Protecting Federated Learning from Extreme Model Poisoning Attacks via Multidimensional Time Series Anomaly Detection + +> Note: If you use this baseline in your work, please remember to cite the original authors of the paper as well as the Flower paper. + **Paper:** [arxiv.org/abs/2303.16668](https://arxiv.org/abs/2303.16668) **Authors:** Edoardo Gabrielli, Gabriele Tolomei, Dimitri Belli, Vittorio Miori From 065a128d9221b882383a2d312e380eb4fd91e578 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Mon, 5 Aug 2024 12:05:47 +0200 Subject: [PATCH 019/188] feat(framework) Update proto files for FAB delivery (#3942) --- src/proto/flwr/proto/driver.proto | 5 ++ src/proto/flwr/proto/fleet.proto | 4 ++ src/proto/flwr/proto/run.proto | 1 + .../client/grpc_rere_client/grpc_adapter.py | 7 +++ src/py/flwr/proto/driver_pb2.py | 43 ++++++++------- src/py/flwr/proto/driver_pb2.pyi | 8 ++- src/py/flwr/proto/driver_pb2_grpc.py | 35 ++++++++++++ src/py/flwr/proto/driver_pb2_grpc.pyi | 14 +++++ src/py/flwr/proto/fleet_pb2.py | 55 ++++++++++--------- src/py/flwr/proto/fleet_pb2_grpc.py | 35 ++++++++++++ src/py/flwr/proto/fleet_pb2_grpc.pyi | 14 +++++ src/py/flwr/proto/run_pb2.py | 16 +++--- src/py/flwr/proto/run_pb2.pyi | 5 +- .../superlink/driver/driver_servicer.py | 7 +++ .../fleet/grpc_rere/fleet_servicer.py | 7 +++ 15 files changed, 198 insertions(+), 58 deletions(-) diff --git a/src/proto/flwr/proto/driver.proto b/src/proto/flwr/proto/driver.proto index 531d18b4f3ad..63a2f78e6f6d 100644 --- a/src/proto/flwr/proto/driver.proto +++ b/src/proto/flwr/proto/driver.proto @@ -20,6 +20,7 @@ package flwr.proto; import "flwr/proto/node.proto"; import "flwr/proto/task.proto"; import "flwr/proto/run.proto"; +import "flwr/proto/fab.proto"; import "flwr/proto/transport.proto"; service Driver { @@ -37,6 +38,9 @@ service Driver { // Get run details rpc GetRun(GetRunRequest) returns (GetRunResponse) {} + + // Get FAB + rpc GetFab(GetFabRequest) returns (GetFabResponse) {} } // CreateRun @@ -44,6 +48,7 @@ message CreateRunRequest { string fab_id = 1; string fab_version = 2; map override_config = 3; + Fab fab = 4; } message CreateRunResponse { sint64 run_id = 1; } diff --git a/src/proto/flwr/proto/fleet.proto b/src/proto/flwr/proto/fleet.proto index 24f60bb3d825..b87214ac52f3 100644 --- a/src/proto/flwr/proto/fleet.proto +++ b/src/proto/flwr/proto/fleet.proto @@ -20,6 +20,7 @@ package flwr.proto; import "flwr/proto/node.proto"; import "flwr/proto/task.proto"; import "flwr/proto/run.proto"; +import "flwr/proto/fab.proto"; service Fleet { rpc CreateNode(CreateNodeRequest) returns (CreateNodeResponse) {} @@ -37,6 +38,9 @@ service Fleet { rpc PushTaskRes(PushTaskResRequest) returns (PushTaskResResponse) {} rpc GetRun(GetRunRequest) returns (GetRunResponse) {} + + // Get FAB + rpc GetFab(GetFabRequest) returns (GetFabResponse) {} } // CreateNode messages diff --git a/src/proto/flwr/proto/run.proto b/src/proto/flwr/proto/run.proto index f46f7146c846..6adca5c2437b 100644 --- a/src/proto/flwr/proto/run.proto +++ b/src/proto/flwr/proto/run.proto @@ -24,6 +24,7 @@ message Run { string fab_id = 2; string fab_version = 3; map override_config = 4; + string fab_hash = 5; } message GetRunRequest { sint64 run_id = 1; } message GetRunResponse { Run run = 1; } diff --git a/src/py/flwr/client/grpc_rere_client/grpc_adapter.py b/src/py/flwr/client/grpc_rere_client/grpc_adapter.py index 77c3d601020d..fde03943a852 100644 --- a/src/py/flwr/client/grpc_rere_client/grpc_adapter.py +++ b/src/py/flwr/client/grpc_rere_client/grpc_adapter.py @@ -28,6 +28,7 @@ GRPC_ADAPTER_METADATA_SHOULD_EXIT_KEY, ) from flwr.common.version import package_version +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -131,3 +132,9 @@ def GetRun( # pylint: disable=C0103 ) -> GetRunResponse: """.""" return self._send_and_receive(request, GetRunResponse, **kwargs) + + def GetFab( # pylint: disable=C0103 + self, request: GetFabRequest, **kwargs: Any + ) -> GetFabResponse: + """.""" + return self._send_and_receive(request, GetFabResponse, **kwargs) diff --git a/src/py/flwr/proto/driver_pb2.py b/src/py/flwr/proto/driver_pb2.py index 6359e2f7d5fa..dde72620f5bf 100644 --- a/src/py/flwr/proto/driver_pb2.py +++ b/src/py/flwr/proto/driver_pb2.py @@ -15,10 +15,11 @@ from flwr.proto import node_pb2 as flwr_dot_proto_dot_node__pb2 from flwr.proto import task_pb2 as flwr_dot_proto_dot_task__pb2 from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2 +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 from flwr.proto import transport_pb2 as flwr_dot_proto_dot_transport__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66lwr/proto/driver.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x15\x66lwr/proto/task.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xcd\x01\n\x10\x43reateRunRequest\x12\x0e\n\x06\x66\x61\x62_id\x18\x01 \x01(\t\x12\x13\n\x0b\x66\x61\x62_version\x18\x02 \x01(\t\x12I\n\x0foverride_config\x18\x03 \x03(\x0b\x32\x30.flwr.proto.CreateRunRequest.OverrideConfigEntry\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"#\n\x11\x43reateRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"!\n\x0fGetNodesRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"3\n\x10GetNodesResponse\x12\x1f\n\x05nodes\x18\x01 \x03(\x0b\x32\x10.flwr.proto.Node\"@\n\x12PushTaskInsRequest\x12*\n\rtask_ins_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskIns\"\'\n\x13PushTaskInsResponse\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"F\n\x12PullTaskResRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"A\n\x13PullTaskResResponse\x12*\n\rtask_res_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskRes2\x84\x03\n\x06\x44river\x12J\n\tCreateRun\x12\x1c.flwr.proto.CreateRunRequest\x1a\x1d.flwr.proto.CreateRunResponse\"\x00\x12G\n\x08GetNodes\x12\x1b.flwr.proto.GetNodesRequest\x1a\x1c.flwr.proto.GetNodesResponse\"\x00\x12P\n\x0bPushTaskIns\x12\x1e.flwr.proto.PushTaskInsRequest\x1a\x1f.flwr.proto.PushTaskInsResponse\"\x00\x12P\n\x0bPullTaskRes\x12\x1e.flwr.proto.PullTaskResRequest\x1a\x1f.flwr.proto.PullTaskResResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66lwr/proto/driver.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x15\x66lwr/proto/task.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xeb\x01\n\x10\x43reateRunRequest\x12\x0e\n\x06\x66\x61\x62_id\x18\x01 \x01(\t\x12\x13\n\x0b\x66\x61\x62_version\x18\x02 \x01(\t\x12I\n\x0foverride_config\x18\x03 \x03(\x0b\x32\x30.flwr.proto.CreateRunRequest.OverrideConfigEntry\x12\x1c\n\x03\x66\x61\x62\x18\x04 \x01(\x0b\x32\x0f.flwr.proto.Fab\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"#\n\x11\x43reateRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"!\n\x0fGetNodesRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"3\n\x10GetNodesResponse\x12\x1f\n\x05nodes\x18\x01 \x03(\x0b\x32\x10.flwr.proto.Node\"@\n\x12PushTaskInsRequest\x12*\n\rtask_ins_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskIns\"\'\n\x13PushTaskInsResponse\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"F\n\x12PullTaskResRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"A\n\x13PullTaskResResponse\x12*\n\rtask_res_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskRes2\xc7\x03\n\x06\x44river\x12J\n\tCreateRun\x12\x1c.flwr.proto.CreateRunRequest\x1a\x1d.flwr.proto.CreateRunResponse\"\x00\x12G\n\x08GetNodes\x12\x1b.flwr.proto.GetNodesRequest\x1a\x1c.flwr.proto.GetNodesResponse\"\x00\x12P\n\x0bPushTaskIns\x12\x1e.flwr.proto.PushTaskInsRequest\x1a\x1f.flwr.proto.PushTaskInsResponse\"\x00\x12P\n\x0bPullTaskRes\x12\x1e.flwr.proto.PullTaskResRequest\x1a\x1f.flwr.proto.PullTaskResResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x12\x41\n\x06GetFab\x12\x19.flwr.proto.GetFabRequest\x1a\x1a.flwr.proto.GetFabResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -27,24 +28,24 @@ DESCRIPTOR._options = None _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._options = None _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._serialized_options = b'8\001' - _globals['_CREATERUNREQUEST']._serialized_start=136 - _globals['_CREATERUNREQUEST']._serialized_end=341 - _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._serialized_start=268 - _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._serialized_end=341 - _globals['_CREATERUNRESPONSE']._serialized_start=343 - _globals['_CREATERUNRESPONSE']._serialized_end=378 - _globals['_GETNODESREQUEST']._serialized_start=380 - _globals['_GETNODESREQUEST']._serialized_end=413 - _globals['_GETNODESRESPONSE']._serialized_start=415 - _globals['_GETNODESRESPONSE']._serialized_end=466 - _globals['_PUSHTASKINSREQUEST']._serialized_start=468 - _globals['_PUSHTASKINSREQUEST']._serialized_end=532 - _globals['_PUSHTASKINSRESPONSE']._serialized_start=534 - _globals['_PUSHTASKINSRESPONSE']._serialized_end=573 - _globals['_PULLTASKRESREQUEST']._serialized_start=575 - _globals['_PULLTASKRESREQUEST']._serialized_end=645 - _globals['_PULLTASKRESRESPONSE']._serialized_start=647 - _globals['_PULLTASKRESRESPONSE']._serialized_end=712 - _globals['_DRIVER']._serialized_start=715 - _globals['_DRIVER']._serialized_end=1103 + _globals['_CREATERUNREQUEST']._serialized_start=158 + _globals['_CREATERUNREQUEST']._serialized_end=393 + _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._serialized_start=320 + _globals['_CREATERUNREQUEST_OVERRIDECONFIGENTRY']._serialized_end=393 + _globals['_CREATERUNRESPONSE']._serialized_start=395 + _globals['_CREATERUNRESPONSE']._serialized_end=430 + _globals['_GETNODESREQUEST']._serialized_start=432 + _globals['_GETNODESREQUEST']._serialized_end=465 + _globals['_GETNODESRESPONSE']._serialized_start=467 + _globals['_GETNODESRESPONSE']._serialized_end=518 + _globals['_PUSHTASKINSREQUEST']._serialized_start=520 + _globals['_PUSHTASKINSREQUEST']._serialized_end=584 + _globals['_PUSHTASKINSRESPONSE']._serialized_start=586 + _globals['_PUSHTASKINSRESPONSE']._serialized_end=625 + _globals['_PULLTASKRESREQUEST']._serialized_start=627 + _globals['_PULLTASKRESREQUEST']._serialized_end=697 + _globals['_PULLTASKRESRESPONSE']._serialized_start=699 + _globals['_PULLTASKRESRESPONSE']._serialized_end=764 + _globals['_DRIVER']._serialized_start=767 + _globals['_DRIVER']._serialized_end=1222 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/driver_pb2.pyi b/src/py/flwr/proto/driver_pb2.pyi index 748399be4e6b..d025e00474eb 100644 --- a/src/py/flwr/proto/driver_pb2.pyi +++ b/src/py/flwr/proto/driver_pb2.pyi @@ -3,6 +3,7 @@ isort:skip_file """ import builtins +import flwr.proto.fab_pb2 import flwr.proto.node_pb2 import flwr.proto.task_pb2 import flwr.proto.transport_pb2 @@ -35,17 +36,22 @@ class CreateRunRequest(google.protobuf.message.Message): FAB_ID_FIELD_NUMBER: builtins.int FAB_VERSION_FIELD_NUMBER: builtins.int OVERRIDE_CONFIG_FIELD_NUMBER: builtins.int + FAB_FIELD_NUMBER: builtins.int fab_id: typing.Text fab_version: typing.Text @property def override_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... + @property + def fab(self) -> flwr.proto.fab_pb2.Fab: ... def __init__(self, *, fab_id: typing.Text = ..., fab_version: typing.Text = ..., override_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., + fab: typing.Optional[flwr.proto.fab_pb2.Fab] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["fab_id",b"fab_id","fab_version",b"fab_version","override_config",b"override_config"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["fab",b"fab"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["fab",b"fab","fab_id",b"fab_id","fab_version",b"fab_version","override_config",b"override_config"]) -> None: ... global___CreateRunRequest = CreateRunRequest class CreateRunResponse(google.protobuf.message.Message): diff --git a/src/py/flwr/proto/driver_pb2_grpc.py b/src/py/flwr/proto/driver_pb2_grpc.py index 2cd3ebe62a63..6745bc7af62a 100644 --- a/src/py/flwr/proto/driver_pb2_grpc.py +++ b/src/py/flwr/proto/driver_pb2_grpc.py @@ -3,6 +3,7 @@ import grpc from flwr.proto import driver_pb2 as flwr_dot_proto_dot_driver__pb2 +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2 @@ -40,6 +41,11 @@ def __init__(self, channel): request_serializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString, response_deserializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString, ) + self.GetFab = channel.unary_unary( + '/flwr.proto.Driver/GetFab', + request_serializer=flwr_dot_proto_dot_fab__pb2.GetFabRequest.SerializeToString, + response_deserializer=flwr_dot_proto_dot_fab__pb2.GetFabResponse.FromString, + ) class DriverServicer(object): @@ -80,6 +86,13 @@ def GetRun(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetFab(self, request, context): + """Get FAB + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_DriverServicer_to_server(servicer, server): rpc_method_handlers = { @@ -108,6 +121,11 @@ def add_DriverServicer_to_server(servicer, server): request_deserializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.FromString, response_serializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.SerializeToString, ), + 'GetFab': grpc.unary_unary_rpc_method_handler( + servicer.GetFab, + request_deserializer=flwr_dot_proto_dot_fab__pb2.GetFabRequest.FromString, + response_serializer=flwr_dot_proto_dot_fab__pb2.GetFabResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'flwr.proto.Driver', rpc_method_handlers) @@ -202,3 +220,20 @@ def GetRun(request, flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetFab(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/flwr.proto.Driver/GetFab', + flwr_dot_proto_dot_fab__pb2.GetFabRequest.SerializeToString, + flwr_dot_proto_dot_fab__pb2.GetFabResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/py/flwr/proto/driver_pb2_grpc.pyi b/src/py/flwr/proto/driver_pb2_grpc.pyi index 4ff09db588ca..7f9fd0acbd82 100644 --- a/src/py/flwr/proto/driver_pb2_grpc.pyi +++ b/src/py/flwr/proto/driver_pb2_grpc.pyi @@ -4,6 +4,7 @@ isort:skip_file """ import abc import flwr.proto.driver_pb2 +import flwr.proto.fab_pb2 import flwr.proto.run_pb2 import grpc @@ -34,6 +35,11 @@ class DriverStub: flwr.proto.run_pb2.GetRunResponse] """Get run details""" + GetFab: grpc.UnaryUnaryMultiCallable[ + flwr.proto.fab_pb2.GetFabRequest, + flwr.proto.fab_pb2.GetFabResponse] + """Get FAB""" + class DriverServicer(metaclass=abc.ABCMeta): @abc.abstractmethod @@ -76,5 +82,13 @@ class DriverServicer(metaclass=abc.ABCMeta): """Get run details""" pass + @abc.abstractmethod + def GetFab(self, + request: flwr.proto.fab_pb2.GetFabRequest, + context: grpc.ServicerContext, + ) -> flwr.proto.fab_pb2.GetFabResponse: + """Get FAB""" + pass + def add_DriverServicer_to_server(servicer: DriverServicer, server: grpc.Server) -> None: ... diff --git a/src/py/flwr/proto/fleet_pb2.py b/src/py/flwr/proto/fleet_pb2.py index 9763b71fed2f..d1fe719f2d91 100644 --- a/src/py/flwr/proto/fleet_pb2.py +++ b/src/py/flwr/proto/fleet_pb2.py @@ -15,9 +15,10 @@ from flwr.proto import node_pb2 as flwr_dot_proto_dot_node__pb2 from flwr.proto import task_pb2 as flwr_dot_proto_dot_task__pb2 from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2 +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x66lwr/proto/fleet.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x15\x66lwr/proto/task.proto\x1a\x14\x66lwr/proto/run.proto\"*\n\x11\x43reateNodeRequest\x12\x15\n\rping_interval\x18\x01 \x01(\x01\"4\n\x12\x43reateNodeResponse\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\"3\n\x11\x44\x65leteNodeRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\"\x14\n\x12\x44\x65leteNodeResponse\"D\n\x0bPingRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x15\n\rping_interval\x18\x02 \x01(\x01\"\x1f\n\x0cPingResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"F\n\x12PullTaskInsRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"k\n\x13PullTaskInsResponse\x12(\n\treconnect\x18\x01 \x01(\x0b\x32\x15.flwr.proto.Reconnect\x12*\n\rtask_ins_list\x18\x02 \x03(\x0b\x32\x13.flwr.proto.TaskIns\"@\n\x12PushTaskResRequest\x12*\n\rtask_res_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskRes\"\xae\x01\n\x13PushTaskResResponse\x12(\n\treconnect\x18\x01 \x01(\x0b\x32\x15.flwr.proto.Reconnect\x12=\n\x07results\x18\x02 \x03(\x0b\x32,.flwr.proto.PushTaskResResponse.ResultsEntry\x1a.\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\r:\x02\x38\x01\"\x1e\n\tReconnect\x12\x11\n\treconnect\x18\x01 \x01(\x04\x32\xc9\x03\n\x05\x46leet\x12M\n\nCreateNode\x12\x1d.flwr.proto.CreateNodeRequest\x1a\x1e.flwr.proto.CreateNodeResponse\"\x00\x12M\n\nDeleteNode\x12\x1d.flwr.proto.DeleteNodeRequest\x1a\x1e.flwr.proto.DeleteNodeResponse\"\x00\x12;\n\x04Ping\x12\x17.flwr.proto.PingRequest\x1a\x18.flwr.proto.PingResponse\"\x00\x12P\n\x0bPullTaskIns\x12\x1e.flwr.proto.PullTaskInsRequest\x1a\x1f.flwr.proto.PullTaskInsResponse\"\x00\x12P\n\x0bPushTaskRes\x12\x1e.flwr.proto.PushTaskResRequest\x1a\x1f.flwr.proto.PushTaskResResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x66lwr/proto/fleet.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x15\x66lwr/proto/task.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x14\x66lwr/proto/fab.proto\"*\n\x11\x43reateNodeRequest\x12\x15\n\rping_interval\x18\x01 \x01(\x01\"4\n\x12\x43reateNodeResponse\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\"3\n\x11\x44\x65leteNodeRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\"\x14\n\x12\x44\x65leteNodeResponse\"D\n\x0bPingRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x15\n\rping_interval\x18\x02 \x01(\x01\"\x1f\n\x0cPingResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"F\n\x12PullTaskInsRequest\x12\x1e\n\x04node\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x10\n\x08task_ids\x18\x02 \x03(\t\"k\n\x13PullTaskInsResponse\x12(\n\treconnect\x18\x01 \x01(\x0b\x32\x15.flwr.proto.Reconnect\x12*\n\rtask_ins_list\x18\x02 \x03(\x0b\x32\x13.flwr.proto.TaskIns\"@\n\x12PushTaskResRequest\x12*\n\rtask_res_list\x18\x01 \x03(\x0b\x32\x13.flwr.proto.TaskRes\"\xae\x01\n\x13PushTaskResResponse\x12(\n\treconnect\x18\x01 \x01(\x0b\x32\x15.flwr.proto.Reconnect\x12=\n\x07results\x18\x02 \x03(\x0b\x32,.flwr.proto.PushTaskResResponse.ResultsEntry\x1a.\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\r:\x02\x38\x01\"\x1e\n\tReconnect\x12\x11\n\treconnect\x18\x01 \x01(\x04\x32\x8c\x04\n\x05\x46leet\x12M\n\nCreateNode\x12\x1d.flwr.proto.CreateNodeRequest\x1a\x1e.flwr.proto.CreateNodeResponse\"\x00\x12M\n\nDeleteNode\x12\x1d.flwr.proto.DeleteNodeRequest\x1a\x1e.flwr.proto.DeleteNodeResponse\"\x00\x12;\n\x04Ping\x12\x17.flwr.proto.PingRequest\x1a\x18.flwr.proto.PingResponse\"\x00\x12P\n\x0bPullTaskIns\x12\x1e.flwr.proto.PullTaskInsRequest\x1a\x1f.flwr.proto.PullTaskInsResponse\"\x00\x12P\n\x0bPushTaskRes\x12\x1e.flwr.proto.PushTaskResRequest\x1a\x1f.flwr.proto.PushTaskResResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x12\x41\n\x06GetFab\x12\x19.flwr.proto.GetFabRequest\x1a\x1a.flwr.proto.GetFabResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -26,30 +27,30 @@ DESCRIPTOR._options = None _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._options = None _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._serialized_options = b'8\001' - _globals['_CREATENODEREQUEST']._serialized_start=106 - _globals['_CREATENODEREQUEST']._serialized_end=148 - _globals['_CREATENODERESPONSE']._serialized_start=150 - _globals['_CREATENODERESPONSE']._serialized_end=202 - _globals['_DELETENODEREQUEST']._serialized_start=204 - _globals['_DELETENODEREQUEST']._serialized_end=255 - _globals['_DELETENODERESPONSE']._serialized_start=257 - _globals['_DELETENODERESPONSE']._serialized_end=277 - _globals['_PINGREQUEST']._serialized_start=279 - _globals['_PINGREQUEST']._serialized_end=347 - _globals['_PINGRESPONSE']._serialized_start=349 - _globals['_PINGRESPONSE']._serialized_end=380 - _globals['_PULLTASKINSREQUEST']._serialized_start=382 - _globals['_PULLTASKINSREQUEST']._serialized_end=452 - _globals['_PULLTASKINSRESPONSE']._serialized_start=454 - _globals['_PULLTASKINSRESPONSE']._serialized_end=561 - _globals['_PUSHTASKRESREQUEST']._serialized_start=563 - _globals['_PUSHTASKRESREQUEST']._serialized_end=627 - _globals['_PUSHTASKRESRESPONSE']._serialized_start=630 - _globals['_PUSHTASKRESRESPONSE']._serialized_end=804 - _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._serialized_start=758 - _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._serialized_end=804 - _globals['_RECONNECT']._serialized_start=806 - _globals['_RECONNECT']._serialized_end=836 - _globals['_FLEET']._serialized_start=839 - _globals['_FLEET']._serialized_end=1296 + _globals['_CREATENODEREQUEST']._serialized_start=128 + _globals['_CREATENODEREQUEST']._serialized_end=170 + _globals['_CREATENODERESPONSE']._serialized_start=172 + _globals['_CREATENODERESPONSE']._serialized_end=224 + _globals['_DELETENODEREQUEST']._serialized_start=226 + _globals['_DELETENODEREQUEST']._serialized_end=277 + _globals['_DELETENODERESPONSE']._serialized_start=279 + _globals['_DELETENODERESPONSE']._serialized_end=299 + _globals['_PINGREQUEST']._serialized_start=301 + _globals['_PINGREQUEST']._serialized_end=369 + _globals['_PINGRESPONSE']._serialized_start=371 + _globals['_PINGRESPONSE']._serialized_end=402 + _globals['_PULLTASKINSREQUEST']._serialized_start=404 + _globals['_PULLTASKINSREQUEST']._serialized_end=474 + _globals['_PULLTASKINSRESPONSE']._serialized_start=476 + _globals['_PULLTASKINSRESPONSE']._serialized_end=583 + _globals['_PUSHTASKRESREQUEST']._serialized_start=585 + _globals['_PUSHTASKRESREQUEST']._serialized_end=649 + _globals['_PUSHTASKRESRESPONSE']._serialized_start=652 + _globals['_PUSHTASKRESRESPONSE']._serialized_end=826 + _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._serialized_start=780 + _globals['_PUSHTASKRESRESPONSE_RESULTSENTRY']._serialized_end=826 + _globals['_RECONNECT']._serialized_start=828 + _globals['_RECONNECT']._serialized_end=858 + _globals['_FLEET']._serialized_start=861 + _globals['_FLEET']._serialized_end=1385 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/fleet_pb2_grpc.py b/src/py/flwr/proto/fleet_pb2_grpc.py index e0b0fbc50460..5f4bb6732dcf 100644 --- a/src/py/flwr/proto/fleet_pb2_grpc.py +++ b/src/py/flwr/proto/fleet_pb2_grpc.py @@ -2,6 +2,7 @@ """Client and server classes corresponding to protobuf-defined services.""" import grpc +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 from flwr.proto import fleet_pb2 as flwr_dot_proto_dot_fleet__pb2 from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2 @@ -45,6 +46,11 @@ def __init__(self, channel): request_serializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString, response_deserializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString, ) + self.GetFab = channel.unary_unary( + '/flwr.proto.Fleet/GetFab', + request_serializer=flwr_dot_proto_dot_fab__pb2.GetFabRequest.SerializeToString, + response_deserializer=flwr_dot_proto_dot_fab__pb2.GetFabResponse.FromString, + ) class FleetServicer(object): @@ -92,6 +98,13 @@ def GetRun(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetFab(self, request, context): + """Get FAB + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_FleetServicer_to_server(servicer, server): rpc_method_handlers = { @@ -125,6 +138,11 @@ def add_FleetServicer_to_server(servicer, server): request_deserializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.FromString, response_serializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.SerializeToString, ), + 'GetFab': grpc.unary_unary_rpc_method_handler( + servicer.GetFab, + request_deserializer=flwr_dot_proto_dot_fab__pb2.GetFabRequest.FromString, + response_serializer=flwr_dot_proto_dot_fab__pb2.GetFabResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'flwr.proto.Fleet', rpc_method_handlers) @@ -236,3 +254,20 @@ def GetRun(request, flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetFab(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/flwr.proto.Fleet/GetFab', + flwr_dot_proto_dot_fab__pb2.GetFabRequest.SerializeToString, + flwr_dot_proto_dot_fab__pb2.GetFabResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/py/flwr/proto/fleet_pb2_grpc.pyi b/src/py/flwr/proto/fleet_pb2_grpc.pyi index 1c0ab862d45c..7988fd6a1dda 100644 --- a/src/py/flwr/proto/fleet_pb2_grpc.pyi +++ b/src/py/flwr/proto/fleet_pb2_grpc.pyi @@ -3,6 +3,7 @@ isort:skip_file """ import abc +import flwr.proto.fab_pb2 import flwr.proto.fleet_pb2 import flwr.proto.run_pb2 import grpc @@ -41,6 +42,11 @@ class FleetStub: flwr.proto.run_pb2.GetRunRequest, flwr.proto.run_pb2.GetRunResponse] + GetFab: grpc.UnaryUnaryMultiCallable[ + flwr.proto.fab_pb2.GetFabRequest, + flwr.proto.fab_pb2.GetFabResponse] + """Get FAB""" + class FleetServicer(metaclass=abc.ABCMeta): @abc.abstractmethod @@ -89,5 +95,13 @@ class FleetServicer(metaclass=abc.ABCMeta): context: grpc.ServicerContext, ) -> flwr.proto.run_pb2.GetRunResponse: ... + @abc.abstractmethod + def GetFab(self, + request: flwr.proto.fab_pb2.GetFabRequest, + context: grpc.ServicerContext, + ) -> flwr.proto.fab_pb2.GetFabResponse: + """Get FAB""" + pass + def add_FleetServicer_to_server(servicer: FleetServicer, server: grpc.Server) -> None: ... diff --git a/src/py/flwr/proto/run_pb2.py b/src/py/flwr/proto/run_pb2.py index c4bf382f1cf9..4892091a6a46 100644 --- a/src/py/flwr/proto/run_pb2.py +++ b/src/py/flwr/proto/run_pb2.py @@ -15,7 +15,7 @@ from flwr.proto import transport_pb2 as flwr_dot_proto_dot_transport__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x66lwr/proto/run.proto\x12\nflwr.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xc3\x01\n\x03Run\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x12\x0e\n\x06\x66\x61\x62_id\x18\x02 \x01(\t\x12\x13\n\x0b\x66\x61\x62_version\x18\x03 \x01(\t\x12<\n\x0foverride_config\x18\x04 \x03(\x0b\x32#.flwr.proto.Run.OverrideConfigEntry\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\x1f\n\rGetRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\".\n\x0eGetRunResponse\x12\x1c\n\x03run\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Runb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x66lwr/proto/run.proto\x12\nflwr.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xd5\x01\n\x03Run\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x12\x0e\n\x06\x66\x61\x62_id\x18\x02 \x01(\t\x12\x13\n\x0b\x66\x61\x62_version\x18\x03 \x01(\t\x12<\n\x0foverride_config\x18\x04 \x03(\x0b\x32#.flwr.proto.Run.OverrideConfigEntry\x12\x10\n\x08\x66\x61\x62_hash\x18\x05 \x01(\t\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\x1f\n\rGetRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\".\n\x0eGetRunResponse\x12\x1c\n\x03run\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Runb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -25,11 +25,11 @@ _globals['_RUN_OVERRIDECONFIGENTRY']._options = None _globals['_RUN_OVERRIDECONFIGENTRY']._serialized_options = b'8\001' _globals['_RUN']._serialized_start=65 - _globals['_RUN']._serialized_end=260 - _globals['_RUN_OVERRIDECONFIGENTRY']._serialized_start=187 - _globals['_RUN_OVERRIDECONFIGENTRY']._serialized_end=260 - _globals['_GETRUNREQUEST']._serialized_start=262 - _globals['_GETRUNREQUEST']._serialized_end=293 - _globals['_GETRUNRESPONSE']._serialized_start=295 - _globals['_GETRUNRESPONSE']._serialized_end=341 + _globals['_RUN']._serialized_end=278 + _globals['_RUN_OVERRIDECONFIGENTRY']._serialized_start=205 + _globals['_RUN_OVERRIDECONFIGENTRY']._serialized_end=278 + _globals['_GETRUNREQUEST']._serialized_start=280 + _globals['_GETRUNREQUEST']._serialized_end=311 + _globals['_GETRUNRESPONSE']._serialized_start=313 + _globals['_GETRUNRESPONSE']._serialized_end=359 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/run_pb2.pyi b/src/py/flwr/proto/run_pb2.pyi index 4db1645da5e2..e65feee9c518 100644 --- a/src/py/flwr/proto/run_pb2.pyi +++ b/src/py/flwr/proto/run_pb2.pyi @@ -33,19 +33,22 @@ class Run(google.protobuf.message.Message): FAB_ID_FIELD_NUMBER: builtins.int FAB_VERSION_FIELD_NUMBER: builtins.int OVERRIDE_CONFIG_FIELD_NUMBER: builtins.int + FAB_HASH_FIELD_NUMBER: builtins.int run_id: builtins.int fab_id: typing.Text fab_version: typing.Text @property def override_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... + fab_hash: typing.Text def __init__(self, *, run_id: builtins.int = ..., fab_id: typing.Text = ..., fab_version: typing.Text = ..., override_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., + fab_hash: typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["fab_id",b"fab_id","fab_version",b"fab_version","override_config",b"override_config","run_id",b"run_id"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["fab_hash",b"fab_hash","fab_id",b"fab_id","fab_version",b"fab_version","override_config",b"override_config","run_id",b"run_id"]) -> None: ... global___Run = Run class GetRunRequest(google.protobuf.message.Message): diff --git a/src/py/flwr/server/superlink/driver/driver_servicer.py b/src/py/flwr/server/superlink/driver/driver_servicer.py index 0741138d2dd1..7819c587e850 100644 --- a/src/py/flwr/server/superlink/driver/driver_servicer.py +++ b/src/py/flwr/server/superlink/driver/driver_servicer.py @@ -35,6 +35,7 @@ PushTaskInsRequest, PushTaskInsResponse, ) +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.node_pb2 import Node # pylint: disable=E0611 from flwr.proto.run_pb2 import ( # pylint: disable=E0611 GetRunRequest, @@ -163,6 +164,12 @@ def GetRun( ) ) + def GetFab( + self, request: GetFabRequest, context: grpc.ServicerContext + ) -> GetFabResponse: + """Will be implemented later.""" + raise NotImplementedError + def _raise_if(validation_error: bool, detail: str) -> None: if validation_error: diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py index 89342a46eb48..a53124124c29 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py @@ -21,6 +21,7 @@ from flwr.common.logger import log from flwr.proto import fleet_pb2_grpc # pylint: disable=E0611 +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -101,3 +102,9 @@ def GetRun( request=request, state=self.state_factory.state(), ) + + def GetFab( + self, request: GetFabRequest, context: grpc.ServicerContext + ) -> GetFabResponse: + """Will be implemented later.""" + raise NotImplementedError From 24e6a629702e32392c6b1f17c7b5dea5de9fcb89 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 5 Aug 2024 18:15:35 +0100 Subject: [PATCH 020/188] refactor(examples) Update `quickstart-pytorch` example (#3918) --- examples/quickstart-pytorch/README.md | 94 ++++------- examples/quickstart-pytorch/client.py | 152 ------------------ examples/quickstart-pytorch/pyproject.toml | 39 +++-- .../pytorchexample/__init__.py | 1 + .../pytorchexample/client_app.py | 65 ++++++++ .../pytorchexample/server_app.py | 45 ++++++ .../quickstart-pytorch/pytorchexample/task.py | 114 +++++++++++++ examples/quickstart-pytorch/server.py | 41 ----- 8 files changed, 285 insertions(+), 266 deletions(-) delete mode 100644 examples/quickstart-pytorch/client.py create mode 100644 examples/quickstart-pytorch/pytorchexample/__init__.py create mode 100644 examples/quickstart-pytorch/pytorchexample/client_app.py create mode 100644 examples/quickstart-pytorch/pytorchexample/server_app.py create mode 100644 examples/quickstart-pytorch/pytorchexample/task.py delete mode 100644 examples/quickstart-pytorch/server.py diff --git a/examples/quickstart-pytorch/README.md b/examples/quickstart-pytorch/README.md index 8eace1ea6845..e37d49194b01 100644 --- a/examples/quickstart-pytorch/README.md +++ b/examples/quickstart-pytorch/README.md @@ -4,94 +4,64 @@ dataset: [CIFAR-10] framework: [torch, torchvision] --- -# Flower Example using PyTorch +# Federated Learning with PyTorch and Flower (Quickstart Example) This introductory example to Flower uses PyTorch, but deep knowledge of PyTorch is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the CIFAR-10 dataset. -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/quickstart-pytorch . && rm -rf flower && cd quickstart-pytorch -``` - -This will create a new directory called `quickstart-pytorch` containing the following files: +Start by cloning the example project: ```shell --- pyproject.toml --- client.py --- server.py --- README.md +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-pytorch . \ + && rm -rf _tmp \ + && cd quickstart-pytorch ``` -### Installing Dependencies - -Project dependencies (such as `torch` and `flwr`) are defined in `pyproject.toml`. You can install the dependencies by invoking `pip`: +This will create a new directory called `quickstart-pytorch` with the following structure: ```shell -# From a new python environment, run: -pip install . +quickstart-pytorch +β”œβ”€β”€ pytorchexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -Then, to verify that everything works correctly you can run the following command: - -```shell -python3 -c "import flwr" -``` +### Install dependencies and project -If you don't see any errors you're good to go! - -______________________________________________________________________ - -## Run Federated Learning with PyTorch and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: - -```shell -python3 server.py -``` +Install the dependencies defined in `pyproject.toml` as well as the `pytorchexample` package. -Now you are ready to start the Flower clients which will participate in the learning. We need to specify the partition id to -use different partitions of the data on different nodes. To do so simply open two more terminal windows and run the -following commands. - -Start client 1 in the first terminal: - -```shell -python3 client.py --partition-id 0 -``` - -Start client 2 in the second terminal: - -```shell -python3 client.py --partition-id 1 +```bash +pip install -e . ``` -You will see that PyTorch is starting a federated training. Look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) for a detailed explanation. - -______________________________________________________________________ +## Run the project -## Run Federated Learning with PyTorch and `Flower Next` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -### 1. Start the long-running Flower server (SuperLink) +### Run with the Simulation Engine ```bash -flower-superlink --insecure +flwr run . ``` -### 2. Start the long-running Flower clients (SuperNodes) - -Start 2 Flower `SuperNodes` in 2 separate terminal windows, using: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flower-client-app client:app --insecure +flwr run . --run-config num-server-rounds=5,learning-rate=0.05 ``` -### 3. Run the Flower App +> \[!TIP\] +> For a more detailed walk-through check our [quickstart PyTorch tutorial](https://flower.ai/docs/framework/tutorial-quickstart-pytorch.html) -With both the long-running server (SuperLink) and two clients (SuperNode) up and running, we can now run the actual Flower App: +### Run with the Deployment Engine -```bash -flower-server-app server:app --insecure -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-pytorch/client.py b/examples/quickstart-pytorch/client.py deleted file mode 100644 index 2452db819e1d..000000000000 --- a/examples/quickstart-pytorch/client.py +++ /dev/null @@ -1,152 +0,0 @@ -import argparse -import warnings -from collections import OrderedDict - -from flwr.client import NumPyClient, ClientApp -from flwr_datasets import FederatedDataset -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch.utils.data import DataLoader -from torchvision.transforms import Compose, Normalize, ToTensor -from tqdm import tqdm - - -# ############################################################################# -# 1. Regular PyTorch pipeline: nn.Module, train, test, and DataLoader -# ############################################################################# - -warnings.filterwarnings("ignore", category=UserWarning) -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -class Net(nn.Module): - """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" - - def __init__(self) -> None: - super(Net, self).__init__() - self.conv1 = nn.Conv2d(3, 6, 5) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, 5) - self.fc1 = nn.Linear(16 * 5 * 5, 120) - self.fc2 = nn.Linear(120, 84) - self.fc3 = nn.Linear(84, 10) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = x.view(-1, 16 * 5 * 5) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - return self.fc3(x) - - -def train(net, trainloader, epochs): - """Train the model on the training set.""" - criterion = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) - for _ in range(epochs): - for batch in tqdm(trainloader, "Training"): - images = batch["img"] - labels = batch["label"] - optimizer.zero_grad() - criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() - optimizer.step() - - -def test(net, testloader): - """Validate the model on the test set.""" - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - with torch.no_grad(): - for batch in tqdm(testloader, "Testing"): - images = batch["img"].to(DEVICE) - labels = batch["label"].to(DEVICE) - outputs = net(images) - loss += criterion(outputs, labels).item() - correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() - accuracy = correct / len(testloader.dataset) - return loss, accuracy - - -def load_data(partition_id): - """Load partition CIFAR10 data.""" - fds = FederatedDataset(dataset="cifar10", partitioners={"train": 2}) - partition = fds.load_partition(partition_id) - # Divide data on each node: 80% train, 20% test - partition_train_test = partition.train_test_split(test_size=0.2, seed=42) - pytorch_transforms = Compose( - [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - def apply_transforms(batch): - """Apply transforms to the partition from FederatedDataset.""" - batch["img"] = [pytorch_transforms(img) for img in batch["img"]] - return batch - - partition_train_test = partition_train_test.with_transform(apply_transforms) - trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) - testloader = DataLoader(partition_train_test["test"], batch_size=32) - return trainloader, testloader - - -# ############################################################################# -# 2. Federation of the pipeline with Flower -# ############################################################################# - -# Get partition id -parser = argparse.ArgumentParser(description="Flower") -parser.add_argument( - "--partition-id", - choices=[0, 1], - default=0, - type=int, - help="Partition of the dataset divided into 2 iid partitions created artificially.", -) -partition_id = parser.parse_known_args()[0].partition_id - -# Load model and data (simple CNN, CIFAR-10) -net = Net().to(DEVICE) -trainloader, testloader = load_data(partition_id=partition_id) - - -# Define Flower client -class FlowerClient(NumPyClient): - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - train(net, trainloader, epochs=1) - return self.get_parameters(config={}), len(trainloader.dataset), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, accuracy = test(net, testloader) - return loss, len(testloader.dataset), {"accuracy": accuracy} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - return FlowerClient().to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, -) - - -# Legacy mode -if __name__ == "__main__": - from flwr.client import start_client - - start_client( - server_address="127.0.0.1:8080", - client=FlowerClient().to_client(), - ) diff --git a/examples/quickstart-pytorch/pyproject.toml b/examples/quickstart-pytorch/pyproject.toml index 89a5cd16d7de..29414962ba6b 100644 --- a/examples/quickstart-pytorch/pyproject.toml +++ b/examples/quickstart-pytorch/pyproject.toml @@ -3,19 +3,36 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-pytorch" -version = "0.1.0" -description = "PyTorch Federated Learning Quickstart with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +name = "pytorchexample" +version = "1.0.0" +description = "Federated Learning with PyTorch and Flower (Quickstart Example)" +license = "Apache-2.0" dependencies = [ - "flwr>=1.8.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", - "torch==2.1.1", - "torchvision==0.16.1", - "tqdm==4.66.3" + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", ] [tool.hatch.build.targets.wheel] packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "pytorchexample.server_app:app" +clientapp = "pytorchexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +fraction-evaluate = 0.5 +local-epochs = 1 +learning-rate = 0.1 +batch-size = 32 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/quickstart-pytorch/pytorchexample/__init__.py b/examples/quickstart-pytorch/pytorchexample/__init__.py new file mode 100644 index 000000000000..d29a98ebc6ae --- /dev/null +++ b/examples/quickstart-pytorch/pytorchexample/__init__.py @@ -0,0 +1 @@ +"""pytorchexample.""" diff --git a/examples/quickstart-pytorch/pytorchexample/client_app.py b/examples/quickstart-pytorch/pytorchexample/client_app.py new file mode 100644 index 000000000000..35ab41abdaf4 --- /dev/null +++ b/examples/quickstart-pytorch/pytorchexample/client_app.py @@ -0,0 +1,65 @@ +"""pytorchexample: A Flower / PyTorch app.""" + +import torch +from flwr.client import NumPyClient, ClientApp +from flwr.common import Context + +from pytorchexample.task import ( + Net, + load_data, + get_weights, + set_weights, + train, + test, +) + + +# Define Flower Client +class FlowerClient(NumPyClient): + def __init__(self, trainloader, valloader, local_epochs, learning_rate): + self.net = Net() + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.lr = learning_rate + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + def fit(self, parameters, config): + """Train the model with data of this client.""" + set_weights(self.net, parameters) + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.lr, + self.device, + ) + return get_weights(self.net), len(self.trainloader.dataset), results + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + # Read run_config to fetch hyperparameters relevant to this run + batch_size = context.run_config["batch-size"] + trainloader, valloader = load_data(partition_id, num_partitions, batch_size) + local_epochs = context.run_config["local-epochs"] + learning_rate = context.run_config["learning-rate"] + + # Return Client instance + return FlowerClient(trainloader, valloader, local_epochs, learning_rate).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn) diff --git a/examples/quickstart-pytorch/pytorchexample/server_app.py b/examples/quickstart-pytorch/pytorchexample/server_app.py new file mode 100644 index 000000000000..faf3293fe272 --- /dev/null +++ b/examples/quickstart-pytorch/pytorchexample/server_app.py @@ -0,0 +1,45 @@ +"""pytorchexample: A Flower / PyTorch app.""" + +from typing import List, Tuple +from flwr.common import Context, ndarrays_to_parameters, Metrics +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from pytorchexample.task import Net, get_weights + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + """Construct components that set the ServerApp behaviour.""" + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define the strategy + strategy = FedAvg( + fraction_fit=1.0, + fraction_evaluate=context.run_config["fraction-evaluate"], + min_available_clients=2, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-pytorch/pytorchexample/task.py b/examples/quickstart-pytorch/pytorchexample/task.py new file mode 100644 index 000000000000..ade5856a7b99 --- /dev/null +++ b/examples/quickstart-pytorch/pytorchexample/task.py @@ -0,0 +1,114 @@ +"""pytorchexample: A Flower / PyTorch app.""" + +from collections import OrderedDict + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int, batch_size: int): + """Load partition CIFAR10 data.""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader( + partition_train_test["train"], batch_size=batch_size, shuffle=True + ) + testloader = DataLoader(partition_train_test["test"], batch_size=batch_size) + return trainloader, testloader + + +def train(net, trainloader, valloader, epochs, learning_rate, device): + """Train the model on the training set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss().to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) + net.train() + for _ in range(epochs): + for batch in trainloader: + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + criterion(net(images.to(device)), labels.to(device)).backward() + optimizer.step() + + val_loss, val_acc = test(net, valloader, device) + + results = { + "val_loss": val_loss, + "val_accuracy": val_acc, + } + return results + + +def test(net, testloader, device): + """Validate the model on the test set.""" + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + loss = loss / len(testloader) + return loss, accuracy diff --git a/examples/quickstart-pytorch/server.py b/examples/quickstart-pytorch/server.py deleted file mode 100644 index 4034703ca690..000000000000 --- a/examples/quickstart-pytorch/server.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List, Tuple - -from flwr.server import ServerApp, ServerConfig -from flwr.server.strategy import FedAvg -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - - -# Define config -config = ServerConfig(num_rounds=3) - - -# Flower ServerApp -app = ServerApp( - config=config, - strategy=strategy, -) - - -# Legacy mode -if __name__ == "__main__": - from flwr.server import start_server - - start_server( - server_address="0.0.0.0:8080", - config=config, - strategy=strategy, - ) From 7ca71f5946c6600047209993aec5af56f7337b81 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Mon, 5 Aug 2024 19:22:31 +0200 Subject: [PATCH 021/188] refactor(framework:skip) Improve templates docstrings (#3894) --- src/py/flwr/cli/new/new.py | 11 ++++++----- src/py/flwr/cli/new/new_test.py | 1 + src/py/flwr/cli/new/templates/app/README.md.tpl | 2 +- .../flwr/cli/new/templates/app/code/__init__.py.tpl | 2 +- .../new/templates/app/code/client.huggingface.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/client.jax.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/client.mlx.py.tpl | 2 +- .../cli/new/templates/app/code/client.numpy.py.tpl | 2 +- .../cli/new/templates/app/code/client.pytorch.py.tpl | 2 +- .../cli/new/templates/app/code/client.sklearn.py.tpl | 2 +- .../new/templates/app/code/client.tensorflow.py.tpl | 2 +- .../new/templates/app/code/server.huggingface.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/server.jax.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/server.mlx.py.tpl | 2 +- .../cli/new/templates/app/code/server.numpy.py.tpl | 2 +- .../cli/new/templates/app/code/server.pytorch.py.tpl | 2 +- .../cli/new/templates/app/code/server.sklearn.py.tpl | 2 +- .../new/templates/app/code/server.tensorflow.py.tpl | 2 +- .../new/templates/app/code/task.huggingface.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/task.jax.py.tpl | 2 +- .../flwr/cli/new/templates/app/code/task.mlx.py.tpl | 2 +- .../cli/new/templates/app/code/task.pytorch.py.tpl | 2 +- .../cli/new/templates/app/code/task.tensorflow.py.tpl | 2 +- 23 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 237b8847e193..1ae574b68b62 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -135,7 +135,7 @@ def new( username = prompt_text("Please provide your Flower username") if framework is not None: - framework_str = str(framework.value) + framework_str_upper = str(framework.value) else: framework_value = prompt_options( "Please select ML framework by typing in the number", @@ -146,9 +146,9 @@ def new( for name, value in vars(MlFramework).items() if value == framework_value ] - framework_str = selected_value[0] + framework_str_upper = selected_value[0] - framework_str = framework_str.lower() + framework_str = framework_str_upper.lower() llm_challenge_str = None if framework_str == "flowertune": @@ -173,9 +173,10 @@ def new( ) context = { - "project_name": project_name, - "package_name": package_name, + "framework_str": framework_str_upper, "import_name": import_name.replace("-", "_"), + "package_name": package_name, + "project_name": project_name, "username": username, } diff --git a/src/py/flwr/cli/new/new_test.py b/src/py/flwr/cli/new/new_test.py index dea10ab35013..62b258c736e6 100644 --- a/src/py/flwr/cli/new/new_test.py +++ b/src/py/flwr/cli/new/new_test.py @@ -39,6 +39,7 @@ def test_render_template() -> None: # Prepare filename = "app/README.md.tpl" data = { + "framework_str": "", "project_name": "FedGPT", "package_name": "fedgpt", "import_name": "fedgpt", diff --git a/src/py/flwr/cli/new/templates/app/README.md.tpl b/src/py/flwr/cli/new/templates/app/README.md.tpl index ddc42cafabc3..40d83f5161e3 100644 --- a/src/py/flwr/cli/new/templates/app/README.md.tpl +++ b/src/py/flwr/cli/new/templates/app/README.md.tpl @@ -1,4 +1,4 @@ -# $project_name +# $project_name: A Flower / $framework_str app ## Install dependencies diff --git a/src/py/flwr/cli/new/templates/app/code/__init__.py.tpl b/src/py/flwr/cli/new/templates/app/code/__init__.py.tpl index 57998c81efb8..e6b63ee8ae6b 100644 --- a/src/py/flwr/cli/new/templates/app/code/__init__.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/__init__.py.tpl @@ -1 +1 @@ -"""$project_name.""" +"""$project_name: A Flower / $framework_str app.""" diff --git a/src/py/flwr/cli/new/templates/app/code/client.huggingface.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.huggingface.py.tpl index 5a2037897d4d..3041a69e3aaa 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.huggingface.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.huggingface.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / HuggingFace Transformers app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.client import ClientApp, NumPyClient from flwr.common import Context diff --git a/src/py/flwr/cli/new/templates/app/code/client.jax.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.jax.py.tpl index 48b667665f3f..046de57f3cf3 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.jax.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.jax.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / JAX app.""" +"""$project_name: A Flower / $framework_str app.""" import jax from flwr.client import NumPyClient, ClientApp diff --git a/src/py/flwr/cli/new/templates/app/code/client.mlx.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.mlx.py.tpl index a28c59eda232..f3105103842d 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.mlx.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.mlx.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / MLX app.""" +"""$project_name: A Flower / $framework_str app.""" import mlx.core as mx import mlx.nn as nn diff --git a/src/py/flwr/cli/new/templates/app/code/client.numpy.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.numpy.py.tpl index 1dd83e108bb5..e35c3c78f6e2 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.numpy.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.numpy.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / NumPy app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.client import NumPyClient, ClientApp from flwr.common import Context diff --git a/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl index ec7f5ffd9b00..e2ba16d1d029 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / PyTorch app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.client import NumPyClient, ClientApp from flwr.common import Context diff --git a/src/py/flwr/cli/new/templates/app/code/client.sklearn.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.sklearn.py.tpl index af1cdb512bee..2d3d1c7f163a 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.sklearn.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.sklearn.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / Scikit-Learn app.""" +"""$project_name: A Flower / $framework_str app.""" import warnings diff --git a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl index 2e1d55d82aa0..7f4bd48909ec 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / TensorFlow app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.client import NumPyClient, ClientApp from flwr.common import Context diff --git a/src/py/flwr/cli/new/templates/app/code/server.huggingface.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.huggingface.py.tpl index 529c2e66ba77..5491f6616160 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.huggingface.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.huggingface.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / HuggingFace Transformers app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context from flwr.server.strategy import FedAvg diff --git a/src/py/flwr/cli/new/templates/app/code/server.jax.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.jax.py.tpl index c1f53e91eadd..514185fde970 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.jax.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.jax.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / JAX app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context from flwr.server.strategy import FedAvg diff --git a/src/py/flwr/cli/new/templates/app/code/server.mlx.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.mlx.py.tpl index 05bb7b6203b3..c99c72574813 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.mlx.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.mlx.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / MLX app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context from flwr.server import ServerApp, ServerAppComponents, ServerConfig diff --git a/src/py/flwr/cli/new/templates/app/code/server.numpy.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.numpy.py.tpl index 1313712189b3..c99c72574813 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.numpy.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.numpy.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / NumPy app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context from flwr.server import ServerApp, ServerAppComponents, ServerConfig diff --git a/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl index d5f1d9332758..80b84f311247 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / PyTorch app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig diff --git a/src/py/flwr/cli/new/templates/app/code/server.sklearn.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.sklearn.py.tpl index d1c8e1606f8f..678ba9326229 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.sklearn.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.sklearn.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / Scikit-Learn app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context from flwr.server import ServerApp, ServerAppComponents, ServerConfig diff --git a/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl index 023f0b66c055..8e174921207b 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / TensorFlow app.""" +"""$project_name: A Flower / $framework_str app.""" from flwr.common import Context, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig diff --git a/src/py/flwr/cli/new/templates/app/code/task.huggingface.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.huggingface.py.tpl index 51a21dd17418..ad52e2c3fe21 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.huggingface.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.huggingface.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / HuggingFace Transformers app.""" +"""$project_name: A Flower / $framework_str app.""" import warnings from collections import OrderedDict diff --git a/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl index 72fe440c978a..fc6ef9dee3dd 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.jax.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / JAX app.""" +"""$project_name: A Flower / $framework_str app.""" import jax import jax.numpy as jnp diff --git a/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl index ed941adaa484..0369bbf92707 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / MLX app.""" +"""$project_name: A Flower / $framework_str app.""" import mlx.core as mx import mlx.nn as nn diff --git a/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl index bd2fad5be589..61429d5e99fe 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / PyTorch app.""" +"""$project_name: A Flower / $framework_str app.""" from collections import OrderedDict diff --git a/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl index c495774ffeb3..8195d8cb47ec 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl @@ -1,4 +1,4 @@ -"""$project_name: A Flower / TensorFlow app.""" +"""$project_name: A Flower / $framework_str app.""" import os From abd833dd37859371cdbcabde484aafd969ffda37 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 5 Aug 2024 18:28:43 +0100 Subject: [PATCH 022/188] refactor(examples) Update `quickstart-tensorflow` example (#3919) --- examples/quickstart-tensorflow/README.md | 88 +++++++------------ examples/quickstart-tensorflow/client.py | 72 --------------- examples/quickstart-tensorflow/pyproject.toml | 36 ++++++-- examples/quickstart-tensorflow/server.py | 41 --------- .../tfexample/__init__.py | 1 + .../tfexample/client_app.py | 67 ++++++++++++++ .../tfexample/server_app.py | 44 ++++++++++ .../quickstart-tensorflow/tfexample/task.py | 59 +++++++++++++ 8 files changed, 231 insertions(+), 177 deletions(-) delete mode 100644 examples/quickstart-tensorflow/client.py delete mode 100644 examples/quickstart-tensorflow/server.py create mode 100644 examples/quickstart-tensorflow/tfexample/__init__.py create mode 100644 examples/quickstart-tensorflow/tfexample/client_app.py create mode 100644 examples/quickstart-tensorflow/tfexample/server_app.py create mode 100644 examples/quickstart-tensorflow/tfexample/task.py diff --git a/examples/quickstart-tensorflow/README.md b/examples/quickstart-tensorflow/README.md index 386f8bbd96f0..f1fa12a3393c 100644 --- a/examples/quickstart-tensorflow/README.md +++ b/examples/quickstart-tensorflow/README.md @@ -4,87 +4,65 @@ dataset: [CIFAR-10] framework: [tensorflow] --- -# Flower Example using TensorFlow/Keras +# Federated Learning with Tensorflow/Keras and Flower (Quickstart Example) -This introductory example to Flower uses Keras but deep knowledge of Keras is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. +This introductory example to Flower uses Tensorflow/Keras but deep knowledge of this frameworks is required to run the example. However, it will help you understand how to adapt Flower to your use case. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the CIFAR-10 dataset. -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/quickstart-tensorflow . && rm -rf flower && cd quickstart-tensorflow -``` - -This will create a new directory called `quickstart-tensorflow` containing the following files: - -```shell --- pyproject.toml --- client.py --- server.py --- README.md -``` - -### Installing Dependencies - -Project dependencies (such as `tensorflow` and `flwr`) are defined in `pyproject.toml`. You can install the dependencies by invoking `pip`: - -```shell -# From a new python environment, run: -pip install . -``` - -Then, to verify that everything works correctly you can run the following command: +Start by cloning the example project: ```shell -python3 -c "import flwr" +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-tensorflow . \ + && rm -rf _tmp \ + && cd quickstart-tensorflow ``` -If you don't see any errors you're good to go! - -## Run Federated Learning with TensorFlow/Keras and Flower - -Afterward, you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +This will create a new directory called `quickstart-tensorflow` with the following structure: ```shell -python3 server.py +quickstart-tensorflow +β”œβ”€β”€ tfexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminals and run the following command in each: - -```shell -python3 client.py --partition-id 0 -``` +### Install dependencies and project -Start client 2 in the second terminal: +Install the dependencies defined in `pyproject.toml` as well as the `tfhexample` package. -```shell -python3 client.py --partition-id 1 +```bash +pip install -e . ``` -You will see that Keras is starting a federated training. Have a look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-tensorflow) for a detailed explanation. You can add `steps_per_epoch=3` to `model.fit()` if you just want to evaluate that everything works without having to wait for the client-side training to finish (this will save you a lot of time during development). +## Run the project -## Run Federated Learning with TensorFlow/Keras and `Flower Next` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -### 1. Start the long-running Flower server (SuperLink) +### Run with the Simulation Engine ```bash -flower-superlink --insecure +flwr run . ``` -### 2. Start the long-running Flower clients (SuperNodes) - -Start 2 Flower \`SuperNodes in 2 separate terminal windows, using: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flower-client-app client:app --insecure +flwr run . --run-config num-server-rounds=5,learning-rate=0.05 ``` -### 3. Run the Flower App +> \[!TIP\] +> For a more detailed walk-through check our [quickstart TensorFlow tutorial](https://flower.ai/docs/framework/tutorial-quickstart-tensorflow.html) -With both the long-running server (SuperLink) and two clients (SuperNode) up and running, we can now run the actual Flower App, using: +### Run with the Deployment Engine -```bash -flower-server-app server:app --insecure -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-tensorflow/client.py b/examples/quickstart-tensorflow/client.py deleted file mode 100644 index 6b2bd6639ce0..000000000000 --- a/examples/quickstart-tensorflow/client.py +++ /dev/null @@ -1,72 +0,0 @@ -import argparse -import os - -from flwr.client import ClientApp, NumPyClient -import tensorflow as tf -from flwr_datasets import FederatedDataset - -# Make TensorFlow log less verbose -os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" - -# Parse arguments -parser = argparse.ArgumentParser(description="Flower") -parser.add_argument( - "--partition-id", - type=int, - choices=[0, 1, 2], - default=0, - help="Partition of the dataset (0, 1 or 2). " - "The dataset is divided into 3 partitions created artificially.", -) -args, _ = parser.parse_known_args() - -# Load model and data (MobileNetV2, CIFAR-10) -model = tf.keras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None) -model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"]) - -# Download and partition dataset -fds = FederatedDataset(dataset="cifar10", partitioners={"train": 3}) -partition = fds.load_partition(args.partition_id, "train") -partition.set_format("numpy") - -# Divide data on each node: 80% train, 20% test -partition = partition.train_test_split(test_size=0.2, seed=42) -x_train, y_train = partition["train"]["img"] / 255.0, partition["train"]["label"] -x_test, y_test = partition["test"]["img"] / 255.0, partition["test"]["label"] - - -# Define Flower client -class FlowerClient(NumPyClient): - def get_parameters(self, config): - return model.get_weights() - - def fit(self, parameters, config): - model.set_weights(parameters) - model.fit(x_train, y_train, epochs=1, batch_size=32) - return model.get_weights(), len(x_train), {} - - def evaluate(self, parameters, config): - model.set_weights(parameters) - loss, accuracy = model.evaluate(x_test, y_test) - return loss, len(x_test), {"accuracy": accuracy} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - return FlowerClient().to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, -) - - -# Legacy mode -if __name__ == "__main__": - from flwr.client import start_client - - start_client( - server_address="127.0.0.1:8080", - client=FlowerClient().to_client(), - ) diff --git a/examples/quickstart-tensorflow/pyproject.toml b/examples/quickstart-tensorflow/pyproject.toml index c0f71344b2fb..5441dab31a8e 100644 --- a/examples/quickstart-tensorflow/pyproject.toml +++ b/examples/quickstart-tensorflow/pyproject.toml @@ -3,18 +3,36 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-tensorflow" -version = "0.1.0" -description = "Keras Federated Learning Quickstart with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +name = "tfexample" +version = "1.0.0" +description = "Federated Learning with Tensorflow/Keras and Flower (Quickstart Example)" +license = "Apache-2.0" dependencies = [ - "flwr>=1.8.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", "tensorflow-cpu>=2.9.1, != 2.11.1 ; platform_machine == \"x86_64\"", "tensorflow-macos>=2.9.1, != 2.11.1 ; sys_platform == \"darwin\" and platform_machine == \"arm64\"" ] - [tool.hatch.build.targets.wheel] packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "tfexample.server_app:app" +clientapp = "tfexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +local-epochs = 1 +batch-size = 32 +learning-rate = 0.005 +fraction-fit = 0.5 +verbose = false + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/quickstart-tensorflow/server.py b/examples/quickstart-tensorflow/server.py deleted file mode 100644 index 4034703ca690..000000000000 --- a/examples/quickstart-tensorflow/server.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List, Tuple - -from flwr.server import ServerApp, ServerConfig -from flwr.server.strategy import FedAvg -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - - -# Define config -config = ServerConfig(num_rounds=3) - - -# Flower ServerApp -app = ServerApp( - config=config, - strategy=strategy, -) - - -# Legacy mode -if __name__ == "__main__": - from flwr.server import start_server - - start_server( - server_address="0.0.0.0:8080", - config=config, - strategy=strategy, - ) diff --git a/examples/quickstart-tensorflow/tfexample/__init__.py b/examples/quickstart-tensorflow/tfexample/__init__.py new file mode 100644 index 000000000000..38122a4cf9fa --- /dev/null +++ b/examples/quickstart-tensorflow/tfexample/__init__.py @@ -0,0 +1 @@ +"""tfexample.""" diff --git a/examples/quickstart-tensorflow/tfexample/client_app.py b/examples/quickstart-tensorflow/tfexample/client_app.py new file mode 100644 index 000000000000..f727215eca47 --- /dev/null +++ b/examples/quickstart-tensorflow/tfexample/client_app.py @@ -0,0 +1,67 @@ +"""tfexample: A Flower / TensorFlow app.""" + +from flwr.client import NumPyClient, ClientApp +from flwr.common import Context + +from tfexample.task import load_data, load_model + + +# Define Flower Client +class FlowerClient(NumPyClient): + def __init__( + self, + learning_rate, + data, + epochs, + batch_size, + verbose, + ): + self.model = load_model(learning_rate) + self.x_train, self.y_train, self.x_test, self.y_test = data + self.epochs = epochs + self.batch_size = batch_size + self.verbose = verbose + + def get_parameters(self, config): + """Return the parameters of the model of this client.""" + return self.model.get_weights() + + def fit(self, parameters, config): + """Train the model with data of this client.""" + self.model.set_weights(parameters) + self.model.fit( + self.x_train, + self.y_train, + epochs=self.epochs, + batch_size=self.batch_size, + verbose=self.verbose, + ) + return self.model.get_weights(), len(self.x_train), {} + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + self.model.set_weights(parameters) + loss, accuracy = self.model.evaluate(self.x_test, self.y_test, verbose=0) + return loss, len(self.x_test), {"accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + data = load_data(partition_id, num_partitions) + + # Read run_config to fetch hyperparameters relevant to this run + epochs = context.run_config["local-epochs"] + batch_size = context.run_config["batch-size"] + verbose = context.run_config.get("verbose") + learning_rate = context.run_config["learning-rate"] + + # Return Client instance + return FlowerClient(learning_rate, data, epochs, batch_size, verbose).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-tensorflow/tfexample/server_app.py b/examples/quickstart-tensorflow/tfexample/server_app.py new file mode 100644 index 000000000000..b45cb541f174 --- /dev/null +++ b/examples/quickstart-tensorflow/tfexample/server_app.py @@ -0,0 +1,44 @@ +"""tfexample: A Flower / TensorFlow app.""" + +from typing import List, Tuple +from flwr.common import Context, ndarrays_to_parameters, Metrics +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from tfexample.task import load_model + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + """Construct components that set the ServerApp behaviour.""" + + # Let's define the global model and pass it to the strategy + # Note this is optional. + parameters = ndarrays_to_parameters(load_model().get_weights()) + + # Define the strategy + strategy = strategy = FedAvg( + fraction_fit=context.run_config["fraction-fit"], + fraction_evaluate=1.0, + min_available_clients=2, + initial_parameters=parameters, + evaluate_metrics_aggregation_fn=weighted_average, + ) + # Read from config + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-tensorflow/tfexample/task.py b/examples/quickstart-tensorflow/tfexample/task.py new file mode 100644 index 000000000000..c9c6fa66de27 --- /dev/null +++ b/examples/quickstart-tensorflow/tfexample/task.py @@ -0,0 +1,59 @@ +"""tfexample: A Flower / TensorFlow app.""" + +import os + +import keras +from keras import layers +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + + +# Make TensorFlow log less verbose +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" + + +def load_model(learning_rate: float = 0.001): + # Define a simple CNN for CIFAR-10 and set Adam optimizer + model = keras.Sequential( + [ + keras.Input(shape=(32, 32, 3)), + layers.Conv2D(32, kernel_size=(3, 3), activation="relu"), + layers.MaxPooling2D(pool_size=(2, 2)), + layers.Conv2D(64, kernel_size=(3, 3), activation="relu"), + layers.MaxPooling2D(pool_size=(2, 2)), + layers.Flatten(), + layers.Dropout(0.5), + layers.Dense(10, activation="softmax"), + ] + ) + optimizer = keras.optimizers.Adam(learning_rate) + model.compile( + optimizer=optimizer, + loss="sparse_categorical_crossentropy", + metrics=["accuracy"], + ) + return model + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id, num_partitions): + # Download and partition dataset + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id, "train") + partition.set_format("numpy") + + # Divide data on each node: 80% train, 20% test + partition = partition.train_test_split(test_size=0.2) + x_train, y_train = partition["train"]["img"] / 255.0, partition["train"]["label"] + x_test, y_test = partition["test"]["img"] / 255.0, partition["test"]["label"] + + return x_train, y_train, x_test, y_test From 6a639c63145f3cbb787953298ed5cd290d8ca0de Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 5 Aug 2024 19:08:21 +0100 Subject: [PATCH 023/188] refactor(framework) Update `mlx`,`tf` and `pytorch` templates (#3933) Co-authored-by: Daniel J. Beutel --- .../templates/app/code/client.pytorch.py.tpl | 15 +++++---- .../app/code/client.tensorflow.py.tpl | 11 +++---- .../templates/app/code/server.pytorch.py.tpl | 11 ++++--- .../app/code/server.tensorflow.py.tpl | 7 ++-- .../new/templates/app/code/task.mlx.py.tpl | 1 + .../templates/app/code/task.pytorch.py.tpl | 32 ++++++++----------- .../templates/app/code/task.tensorflow.py.tpl | 18 +++++++++-- .../app/pyproject.huggingface.toml.tpl | 4 +-- .../new/templates/app/pyproject.jax.toml.tpl | 2 +- .../new/templates/app/pyproject.mlx.toml.tpl | 6 ++-- .../templates/app/pyproject.numpy.toml.tpl | 2 +- .../templates/app/pyproject.pytorch.toml.tpl | 5 +-- .../templates/app/pyproject.sklearn.toml.tpl | 4 +-- .../app/pyproject.tensorflow.toml.tpl | 4 +-- 14 files changed, 64 insertions(+), 58 deletions(-) diff --git a/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl index e2ba16d1d029..bcade355e22f 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.pytorch.py.tpl @@ -1,11 +1,11 @@ """$project_name: A Flower / $framework_str app.""" +import torch from flwr.client import NumPyClient, ClientApp from flwr.common import Context from $import_name.task import ( Net, - DEVICE, load_data, get_weights, set_weights, @@ -21,27 +21,28 @@ class FlowerClient(NumPyClient): self.trainloader = trainloader self.valloader = valloader self.local_epochs = local_epochs + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.net.to(self.device) def fit(self, parameters, config): set_weights(self.net, parameters) - results = train( + train_loss = train( self.net, self.trainloader, - self.valloader, self.local_epochs, - DEVICE, + self.device, ) - return get_weights(self.net), len(self.trainloader.dataset), results + return get_weights(self.net), len(self.trainloader.dataset), {"train_loss": train_loss} def evaluate(self, parameters, config): set_weights(self.net, parameters) - loss, accuracy = test(self.net, self.valloader) + loss, accuracy = test(self.net, self.valloader, self.device) return loss, len(self.valloader.dataset), {"accuracy": accuracy} def client_fn(context: Context): # Load model and data - net = Net().to(DEVICE) + net = Net() partition_id = context.node_config["partition-id"] num_partitions = context.node_config["num-partitions"] trainloader, valloader = load_data(partition_id, num_partitions) diff --git a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl index 7f4bd48909ec..48ee3b4f5356 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl @@ -9,13 +9,10 @@ from $import_name.task import load_data, load_model # Define Flower Client and client_fn class FlowerClient(NumPyClient): def __init__( - self, model, x_train, y_train, x_test, y_test, epochs, batch_size, verbose + self, model, data, epochs, batch_size, verbose ): self.model = model - self.x_train = x_train - self.y_train = y_train - self.x_test = x_test - self.y_test = y_test + self.x_train, self.y_train, self.x_test, self.y_test = data self.epochs = epochs self.batch_size = batch_size self.verbose = verbose @@ -46,14 +43,14 @@ def client_fn(context: Context): partition_id = context.node_config["partition-id"] num_partitions = context.node_config["num-partitions"] - x_train, y_train, x_test, y_test = load_data(partition_id, num_partitions) + data = load_data(partition_id, num_partitions) epochs = context.run_config["local-epochs"] batch_size = context.run_config["batch-size"] verbose = context.run_config.get("verbose") # Return Client instance return FlowerClient( - net, x_train, y_train, x_test, y_test, epochs, batch_size, verbose + net, data, epochs, batch_size, verbose ).to_client() diff --git a/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl index 80b84f311247..39185965b3a5 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.pytorch.py.tpl @@ -7,17 +7,18 @@ from flwr.server.strategy import FedAvg from $import_name.task import Net, get_weights -# Initialize model parameters -ndarrays = get_weights(Net()) -parameters = ndarrays_to_parameters(ndarrays) - def server_fn(context: Context): # Read from config num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) # Define strategy strategy = FedAvg( - fraction_fit=1.0, + fraction_fit=fraction_fit, fraction_evaluate=1.0, min_available_clients=2, initial_parameters=parameters, diff --git a/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl index 8e174921207b..050da37cf527 100644 --- a/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/server.tensorflow.py.tpl @@ -6,15 +6,14 @@ from flwr.server.strategy import FedAvg from $import_name.task import load_model -# Define config -config = ServerConfig(num_rounds=3) - -parameters = ndarrays_to_parameters(load_model().get_weights()) def server_fn(context: Context): # Read from config num_rounds = context.run_config["num-server-rounds"] + # Get parameters to initialize global model + parameters = ndarrays_to_parameters(load_model().get_weights()) + # Define strategy strategy = strategy = FedAvg( fraction_fit=1.0, diff --git a/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl index 0369bbf92707..f959cd1d64e3 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.mlx.py.tpl @@ -56,6 +56,7 @@ def load_data(partition_id: int, num_partitions: int): fds = FederatedDataset( dataset="ylecun/mnist", partitioners={"train": partitioner}, + trust_remote_code=True, ) partition = fds.load_partition(partition_id) partition_splits = partition.train_test_split(test_size=0.2, seed=42) diff --git a/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl index 61429d5e99fe..5562371ad460 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.pytorch.py.tpl @@ -11,9 +11,6 @@ from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - class Net(nn.Module): """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" @@ -66,44 +63,41 @@ def load_data(partition_id: int, num_partitions: int): return trainloader, testloader -def train(net, trainloader, valloader, epochs, device): +def train(net, trainloader, epochs, device): """Train the model on the training set.""" net.to(device) # move model to GPU if available criterion = torch.nn.CrossEntropyLoss().to(device) - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) + optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9) net.train() + running_loss = 0.0 for _ in range(epochs): for batch in trainloader: images = batch["img"] labels = batch["label"] optimizer.zero_grad() - criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() + loss = criterion(net(images.to(device)), labels.to(device)) + loss.backward() optimizer.step() + running_loss += loss.item() - train_loss, train_acc = test(net, trainloader) - val_loss, val_acc = test(net, valloader) - - results = { - "train_loss": train_loss, - "train_accuracy": train_acc, - "val_loss": val_loss, - "val_accuracy": val_acc, - } - return results + avg_trainloss = running_loss / len(trainloader) + return avg_trainloss -def test(net, testloader): +def test(net, testloader, device): """Validate the model on the test set.""" + net.to(device) criterion = torch.nn.CrossEntropyLoss() correct, loss = 0, 0.0 with torch.no_grad(): for batch in testloader: - images = batch["img"].to(DEVICE) - labels = batch["label"].to(DEVICE) + images = batch["img"].to(device) + labels = batch["label"].to(device) outputs = net(images) loss += criterion(outputs, labels).item() correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() accuracy = correct / len(testloader.dataset) + loss = loss / len(testloader) return loss, accuracy diff --git a/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl index 8195d8cb47ec..cc782b5446ec 100644 --- a/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/task.tensorflow.py.tpl @@ -2,7 +2,8 @@ import os -import tensorflow as tf +import keras +from keras import layers from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner @@ -12,8 +13,19 @@ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" def load_model(): - # Load model and data (MobileNetV2, CIFAR-10) - model = tf.keras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None) + # Define a simple CNN for CIFAR-10 and set Adam optimizer + model = keras.Sequential( + [ + keras.Input(shape=(32, 32, 3)), + layers.Conv2D(32, kernel_size=(3, 3), activation="relu"), + layers.MaxPooling2D(pool_size=(2, 2)), + layers.Conv2D(64, kernel_size=(3, 3), activation="relu"), + layers.MaxPooling2D(pool_size=(2, 2)), + layers.Flatten(), + layers.Dropout(0.5), + layers.Dense(10, activation="softmax"), + ] + ) model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"]) return model diff --git a/src/py/flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl index cd70862ee9bf..15dc2af87a3f 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl @@ -8,8 +8,8 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets>=0.0.2,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets>=0.3.0", "torch==2.2.1", "transformers>=4.30.0,<5.0", "evaluate>=0.4.0,<1.0", diff --git a/src/py/flwr/cli/new/templates/app/pyproject.jax.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.jax.toml.tpl index 5f9cf12be4d9..31fff1c2a4c8 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.jax.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.jax.toml.tpl @@ -8,7 +8,7 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", + "flwr[simulation]>=1.10.0", "jax==0.4.13", "jaxlib==0.4.13", "scikit-learn==1.3.2", diff --git a/src/py/flwr/cli/new/templates/app/pyproject.mlx.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.mlx.toml.tpl index 553e416b27ed..c1bfe804c709 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.mlx.toml.tpl @@ -8,9 +8,9 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", - "mlx==0.10.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "mlx==0.16.1", "numpy==1.24.4", ] diff --git a/src/py/flwr/cli/new/templates/app/pyproject.numpy.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.numpy.toml.tpl index 6d38a64ef778..953e556ad012 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.numpy.toml.tpl @@ -8,7 +8,7 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", + "flwr[simulation]>=1.10.0", "numpy>=1.21.0", ] diff --git a/src/py/flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl index 81283331e189..ccaf88c19e42 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl @@ -8,8 +8,8 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", "torch==2.2.1", "torchvision==0.17.1", ] @@ -26,6 +26,7 @@ clientapp = "$import_name.client_app:app" [tool.flwr.app.config] num-server-rounds = 3 +fraction-fit = 0.5 local-epochs = 1 [tool.flwr.federations] diff --git a/src/py/flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl index ea07d5dad4f7..2b5778fec9a7 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl @@ -8,8 +8,8 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", "scikit-learn>=1.1.1", ] diff --git a/src/py/flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl index d5d520aaf001..11f7d1083abc 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl @@ -8,8 +8,8 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets[vision]>=0.0.2,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", "tensorflow>=2.11.1", ] From 86edf75e5bce36edf4a39f3fb4cdfc884a0275e2 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Mon, 5 Aug 2024 20:59:38 +0200 Subject: [PATCH 024/188] feat(framework:skip) Add `get_fab_config` function (#3943) --- src/py/flwr/cli/config_utils.py | 35 +++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/py/flwr/cli/config_utils.py b/src/py/flwr/cli/config_utils.py index 74eda81f5c16..5e5f56555da4 100644 --- a/src/py/flwr/cli/config_utils.py +++ b/src/py/flwr/cli/config_utils.py @@ -25,8 +25,8 @@ from flwr.common.typing import UserConfigValue -def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]: - """Extract the fab_id and the fab_version from a FAB file or path. +def get_fab_config(fab_file: Union[Path, bytes]) -> Dict[str, Any]: + """Extract the config from a FAB file or path. Parameters ---------- @@ -36,8 +36,8 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]: Returns ------- - Tuple[str, str] - The `fab_version` and `fab_id` of the given Flower App Bundle. + Dict[str, Any] + The `config` of the given Flower App Bundle. """ fab_file_archive: Union[Path, IO[bytes]] if isinstance(fab_file, bytes): @@ -59,10 +59,29 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]: if not is_valid: raise ValueError(errors) - return ( - conf["project"]["version"], - f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}", - ) + return conf + + +def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]: + """Extract the fab_id and the fab_version from a FAB file or path. + + Parameters + ---------- + fab_file : Union[Path, bytes] + The Flower App Bundle file to validate and extract the metadata from. + It can either be a path to the file or the file itself as bytes. + + Returns + ------- + Tuple[str, str] + The `fab_version` and `fab_id` of the given Flower App Bundle. + """ + conf = get_fab_config(fab_file) + + return ( + conf["project"]["version"], + f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}", + ) def load_and_validate( From 730e71c2242f6d06139f2230bd2b814f6a4eb825 Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 6 Aug 2024 20:09:48 +0100 Subject: [PATCH 025/188] refactor(framework) Change order of frameworks shown with `flwr new` (#3965) --- src/py/flwr/cli/new/new.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 1ae574b68b62..23b69de17a9b 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -34,13 +34,13 @@ class MlFramework(str, Enum): """Available frameworks.""" - NUMPY = "NumPy" PYTORCH = "PyTorch" TENSORFLOW = "TensorFlow" - JAX = "JAX" + SKLEARN = "sklearn" HUGGINGFACE = "HuggingFace" + JAX = "JAX" MLX = "MLX" - SKLEARN = "sklearn" + NUMPY = "NumPy" FLOWERTUNE = "FlowerTune" @@ -139,7 +139,7 @@ def new( else: framework_value = prompt_options( "Please select ML framework by typing in the number", - sorted([mlf.value for mlf in MlFramework]), + [mlf.value for mlf in MlFramework], ) selected_value = [ name From 2129cb1ee7f4c53e1be4dcd206aa3caf942a7883 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Tue, 6 Aug 2024 21:31:05 +0100 Subject: [PATCH 026/188] docs(*:skip) Update Flower example using Fastai (#3765) Co-authored-by: jafermarq --- examples/quickstart-fastai/.gitignore | 1 - examples/quickstart-fastai/README.md | 82 ++++++++----------- examples/quickstart-fastai/client.py | 49 ----------- .../fastai_example/__init__.py | 1 + .../fastai_example/client_app.py | 57 +++++++++++++ .../fastai_example/server_app.py | 47 +++++++++++ .../quickstart-fastai/fastai_example/task.py | 82 +++++++++++++++++++ examples/quickstart-fastai/pyproject.toml | 46 ++++++++--- examples/quickstart-fastai/requirements.txt | 4 - examples/quickstart-fastai/run.sh | 17 ---- examples/quickstart-fastai/server.py | 25 ------ 11 files changed, 253 insertions(+), 158 deletions(-) delete mode 100644 examples/quickstart-fastai/.gitignore delete mode 100644 examples/quickstart-fastai/client.py create mode 100644 examples/quickstart-fastai/fastai_example/__init__.py create mode 100644 examples/quickstart-fastai/fastai_example/client_app.py create mode 100644 examples/quickstart-fastai/fastai_example/server_app.py create mode 100644 examples/quickstart-fastai/fastai_example/task.py delete mode 100644 examples/quickstart-fastai/requirements.txt delete mode 100755 examples/quickstart-fastai/run.sh delete mode 100644 examples/quickstart-fastai/server.py diff --git a/examples/quickstart-fastai/.gitignore b/examples/quickstart-fastai/.gitignore deleted file mode 100644 index fa6560829782..000000000000 --- a/examples/quickstart-fastai/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.ipynb diff --git a/examples/quickstart-fastai/README.md b/examples/quickstart-fastai/README.md index d1bf97cd4203..977529914e9a 100644 --- a/examples/quickstart-fastai/README.md +++ b/examples/quickstart-fastai/README.md @@ -4,77 +4,61 @@ dataset: [MNIST] framework: [fastai] --- -# Flower Example using fastai +# Federated Learning with fastai and Flower (Quickstart Example) -This introductory example to Flower uses [fastai](https://www.fast.ai/), but deep knowledge of fastai is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. -Running this example in itself is quite easy. +This introductory example to Flower uses [fastai](https://www.fast.ai/), but deep knowledge of fastai is not necessarily required to run the example. The example will help you understand how to adapt Flower to your specific use case, and running it is quite straightforward. -## Project Setup +fastai is a deep learning library built on PyTorch which provides practitioners with high-level components for building deep learning projects. In this example, we will train a [SqueezeNet v1.1](https://github.com/forresti/SqueezeNet/tree/master/SqueezeNet_v1.1) model on the [MNIST](https://huggingface.co/datasets/ylecun/mnist) dataset. The data will be downloaded and partitioned using [Flower Datasets](https://flower.ai/docs/datasets/). -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +## Set up the project -```shell -git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/quickstart-fastai . && rm -rf _tmp && cd quickstart-fastai -``` - -This will create a new directory called `quickstart-fastai` containing the following files: - -```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- run.sh --- README.md -``` - -### Installing Dependencies +### Clone the project -Project dependencies (such as `fastai` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -#### Poetry +Start by cloning the example project: ```shell -poetry install -poetry shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-fastai . \ + && rm -rf _tmp && cd quickstart-fastai ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +This will create a new directory called `quickstart-fastai` containing the following files: ```shell -poetry run python3 -c "import flwr" +quickstart-fastai +β”œβ”€β”€ fastai_example +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -If you don't see any errors you're good to go! +### Install dependencies and project -#### pip +Install the dependencies defined in `pyproject.toml` as well as the `fastai_example` package. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt +```bash +pip install -e . ``` -## Run Federated Learning with fastai and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +## Run the project -```shell -python3 server.py -``` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminal windows and run the following commands. +### Run with the Simulation Engine -Start client 1 in the first terminal: - -```shell -python3 client.py +```bash +flwr run . ``` -Start client 2 in the second terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 client.py +```bash +flwr run . --run-config num-server-rounds=5 ``` -You will see that fastai is starting a federated training. For a more in-depth look, be sure to check out the code on our [repo](https://github.com/adap/flower/tree/main/examples/quickstart-fastai). +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-fastai/client.py b/examples/quickstart-fastai/client.py deleted file mode 100644 index 6bb2a751d544..000000000000 --- a/examples/quickstart-fastai/client.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings -from collections import OrderedDict - -import torch -from fastai.vision.all import * - -import flwr as fl - - -warnings.filterwarnings("ignore", category=UserWarning) - -# Download MNIST dataset -path = untar_data(URLs.MNIST) - -# Load dataset -dls = ImageDataLoaders.from_folder( - path, valid_pct=0.5, train="training", valid="testing", num_workers=0 -) - -# Define model -learn = vision_learner(dls, squeezenet1_1, metrics=error_rate) - - -# Define Flower client -class FlowerClient(fl.client.NumPyClient): - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in learn.model.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(learn.model.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - learn.model.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - learn.fit(1) - return self.get_parameters(config={}), len(dls.train), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, error_rate = learn.validate() - return loss, len(dls.valid), {"accuracy": 1 - error_rate} - - -# Start Flower client -fl.client.start_client( - server_address="127.0.0.1:8080", - client=FlowerClient().to_client(), -) diff --git a/examples/quickstart-fastai/fastai_example/__init__.py b/examples/quickstart-fastai/fastai_example/__init__.py new file mode 100644 index 000000000000..14ef80393289 --- /dev/null +++ b/examples/quickstart-fastai/fastai_example/__init__.py @@ -0,0 +1 @@ +"""fastai_example: A Flower / Fastai app.""" diff --git a/examples/quickstart-fastai/fastai_example/client_app.py b/examples/quickstart-fastai/fastai_example/client_app.py new file mode 100644 index 000000000000..a40f3d5c176d --- /dev/null +++ b/examples/quickstart-fastai/fastai_example/client_app.py @@ -0,0 +1,57 @@ +"""fastai_example: A Flower / Fastai app.""" + +import warnings +from typing import Any + +from fastai.learner import Learner +from fastai.losses import CrossEntropyLossFlat +from fastai.vision.all import error_rate, squeezenet1_1 +from fastai.vision.data import DataLoaders + +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context + +from fastai_example.task import load_data, set_params, get_params + +warnings.filterwarnings("ignore", category=UserWarning) + + +# Define Flower client +class FlowerClient(NumPyClient): + def __init__(self, learn, dls) -> None: + self.learn = learn + self.dls = dls + + def fit(self, parameters, config) -> tuple[list, int, dict]: + set_params(self.learn.model, parameters) + with self.learn.no_bar(), self.learn.no_logging(): + self.learn.fit(1) + return get_params(self.learn.model), len(self.dls.train), {} + + def evaluate(self, parameters, config) -> tuple[Any, int, dict[str, Any]]: + set_params(self.learn.model, parameters) + with self.learn.no_bar(), self.learn.no_logging(): + loss, error_rate = self.learn.validate() + return loss, len(self.dls.valid), {"accuracy": 1 - error_rate} + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + trainloader, valloader, _ = load_data(partition_id, num_partitions) + dls = DataLoaders(trainloader, valloader) + model = squeezenet1_1() + learn = Learner( + dls, + model, + loss_func=CrossEntropyLossFlat(), + metrics=error_rate, + ) + return FlowerClient(learn, dls).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-fastai/fastai_example/server_app.py b/examples/quickstart-fastai/fastai_example/server_app.py new file mode 100644 index 000000000000..0f61319c54b0 --- /dev/null +++ b/examples/quickstart-fastai/fastai_example/server_app.py @@ -0,0 +1,47 @@ +"""fastai_example: A Flower / Fastai app.""" + +from typing import List, Tuple + +from fastai.vision.all import squeezenet1_1 +from flwr.common import Context, Metrics, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from fastai_example.task import get_params + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + """Compute weighted average metric values.""" + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components for ServerApp.""" + + # Let's define the global model and pass it to the strategy + # Note this is optional. + parameters = ndarrays_to_parameters(get_params(squeezenet1_1())) + + # Define strategy + fraction_fit = context.run_config["fraction-fit"] + strategy = FedAvg( + fraction_fit=fraction_fit, + fraction_evaluate=0.5, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(config=config, strategy=strategy) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-fastai/fastai_example/task.py b/examples/quickstart-fastai/fastai_example/task.py new file mode 100644 index 000000000000..397ba0422c36 --- /dev/null +++ b/examples/quickstart-fastai/fastai_example/task.py @@ -0,0 +1,82 @@ +"""fastai_example: A Flower / Fastai app.""" + +from collections import OrderedDict + +import torch +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Lambda, Resize, ToTensor + +fds = None # Cache FederatedDataset + + +def load_data( + partition_id, + num_partitions, +) -> tuple[DataLoader, DataLoader, DataLoader]: + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + trust_remote_code=True, + ) + partition = fds.load_partition(partition_id, "train") + + # Resize and repeat channels to use MNIST, which have grayscale images, + # with squeezenet, which expects 3 channels. + # Ref: https://discuss.pytorch.org/t/fine-tuning-squeezenet-for-mnist-dataset/31221/2 + pytorch_transforms = Compose( + [Resize(224), ToTensor(), Lambda(lambda x: x.expand(3, -1, -1))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["image"] = [pytorch_transforms(img) for img in batch["image"]] + return batch + + def collate_fn(batch): + """Change the dictionary to tuple to keep the exact dataloader behavior.""" + images = [item["image"] for item in batch] + labels = [item["label"] for item in batch] + + images_tensor = torch.stack(images) + labels_tensor = torch.tensor(labels) + + return images_tensor, labels_tensor + + partition = partition.with_transform(apply_transforms) + # 20 % for on federated evaluation + partition_full = partition.train_test_split(test_size=0.2, seed=42) + # 60 % for the federated train and 20 % for the federated validation (both in fit) + partition_train_valid = partition_full["train"].train_test_split( + train_size=0.75, seed=42 + ) + trainloader = DataLoader( + partition_train_valid["train"], + batch_size=32, + shuffle=True, + collate_fn=collate_fn, + ) + valloader = DataLoader( + partition_train_valid["test"], + batch_size=32, + collate_fn=collate_fn, + ) + testloader = DataLoader( + partition_full["test"], batch_size=32, collate_fn=collate_fn, num_workers=1 + ) + return trainloader, valloader, testloader + + +def get_params(model) -> list: + return [val.cpu().numpy() for _, val in model.state_dict().items()] + + +def set_params(model, parameters) -> None: + params_dict = zip(model.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) diff --git a/examples/quickstart-fastai/pyproject.toml b/examples/quickstart-fastai/pyproject.toml index 19a25291a6af..4d160bae0eec 100644 --- a/examples/quickstart-fastai/pyproject.toml +++ b/examples/quickstart-fastai/pyproject.toml @@ -1,16 +1,36 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "quickstart-fastai" -version = "0.1.0" -description = "Fastai Federated Learning Quickstart with Flower" -authors = ["The Flower Authors "] +[project] +name = "fastai_example" +version = "1.0.0" +description = "Federated Learning with Fastai and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "fastai==2.7.14", + "torch==2.2.0", + "torchvision==0.17.0", +] -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -fastai = "2.7.14" -torch = "2.2.0" -torchvision = "0.17.0" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "fastai_example.server_app:app" +clientapp = "fastai_example.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +fraction-fit = 0.5 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/quickstart-fastai/requirements.txt b/examples/quickstart-fastai/requirements.txt deleted file mode 100644 index 9c6e8d77293a..000000000000 --- a/examples/quickstart-fastai/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr>=1.0, <2.0 -fastai==2.7.14 -torch==2.2.0 -torchvision==0.17.0 diff --git a/examples/quickstart-fastai/run.sh b/examples/quickstart-fastai/run.sh deleted file mode 100755 index 3def34c9fcaa..000000000000 --- a/examples/quickstart-fastai/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python client.py & -done - -# Enable CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-fastai/server.py b/examples/quickstart-fastai/server.py deleted file mode 100644 index fe691a88aba0..000000000000 --- a/examples/quickstart-fastai/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List, Tuple - -import flwr as fl -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) From c29e5a8316d1a2bf8880db4c884610b1ed7106f3 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Tue, 6 Aug 2024 21:50:09 +0100 Subject: [PATCH 027/188] refactor(examples) Update PyTorch Lightning quickstart example (#3758) Co-authored-by: jafermarq --- .../quickstart-pytorch-lightning/.gitignore | 1 - .../quickstart-pytorch-lightning/README.md | 77 ++++++---------- .../quickstart-pytorch-lightning/client.py | 80 ---------------- .../pyproject.toml | 48 +++++++--- .../pytorchlightning_example/__init__.py | 1 + .../pytorchlightning_example/client_app.py | 59 ++++++++++++ .../pytorchlightning_example/server_app.py | 31 +++++++ .../task.py} | 91 ++++++++----------- .../requirements.txt | 4 - examples/quickstart-pytorch-lightning/run.sh | 15 --- .../quickstart-pytorch-lightning/server.py | 20 ---- 11 files changed, 193 insertions(+), 234 deletions(-) delete mode 100644 examples/quickstart-pytorch-lightning/client.py create mode 100644 examples/quickstart-pytorch-lightning/pytorchlightning_example/__init__.py create mode 100644 examples/quickstart-pytorch-lightning/pytorchlightning_example/client_app.py create mode 100644 examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py rename examples/quickstart-pytorch-lightning/{mnist.py => pytorchlightning_example/task.py} (56%) delete mode 100644 examples/quickstart-pytorch-lightning/requirements.txt delete mode 100755 examples/quickstart-pytorch-lightning/run.sh delete mode 100644 examples/quickstart-pytorch-lightning/server.py diff --git a/examples/quickstart-pytorch-lightning/.gitignore b/examples/quickstart-pytorch-lightning/.gitignore index 2e0f6a4fa61f..3d38bd5e5f3e 100644 --- a/examples/quickstart-pytorch-lightning/.gitignore +++ b/examples/quickstart-pytorch-lightning/.gitignore @@ -1,2 +1 @@ lightning_logs -MNIST diff --git a/examples/quickstart-pytorch-lightning/README.md b/examples/quickstart-pytorch-lightning/README.md index 04eb911818fc..e520be856962 100644 --- a/examples/quickstart-pytorch-lightning/README.md +++ b/examples/quickstart-pytorch-lightning/README.md @@ -4,79 +4,58 @@ dataset: [MNIST] framework: [lightning] --- -# Flower Example using PyTorch Lightning +# Federated Learning with PyTorch Lightning and Flower (Quickstart Example) -This introductory example to Flower uses PyTorch, but deep knowledge of PyTorch Lightning is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the MNIST dataset. +This introductory example to Flower uses PyTorch Lightning, but deep knowledge of PyTorch Lightning is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the MNIST dataset. The model being federated is a lightweight AutoEncoder as presented in [Lightning in 15 minutes](https://lightning.ai/docs/pytorch/stable/starter/introduction.html) tutorial. ## Project Setup Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: ```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/quickstart-pytorch-lightning . && rm -rf flower && cd quickstart-pytorch-lightning +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-pytorch-lightning . \ + && rm -rf _tmp && cd quickstart-pytorch-lightning ``` This will create a new directory called `quickstart-pytorch-lightning` containing the following files: ```shell --- pyproject.toml --- requirements.txt --- client.py # client-side code --- server.py # server-side code (including the strategy) --- README.md --- run.sh # runs server, then two clients --- mnist.py # run a centralised version of this example +quickstart-pytorch-lightning +β”œβ”€β”€ pytorchlightning_example +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies +# Install dependencies and project -Project dependencies (such as `torch` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +Install the dependencies defined in `pyproject.toml` as well as the `pytorchlightning_example` package. -#### Poetry - -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python -c "import flwr" -``` +## Run the Example -If you don't see any errors you're good to go! +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -#### pip +### Run with the Simulation Engine -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt +```bash +flwr run . ``` -## Run Federated Learning with PyTorch and Flower +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: - -```shell -python server.py +```bash +flwr run . --run-config num-server-rounds=5,max-epochs=2 ``` -Now you are ready to start the Flower clients which will participate in the learning. We need to specify the partition id to -use different partitions of the data on different nodes. To do so simply open two more terminal windows and run the -following commands. - -Start client 1 in the first terminal: - -```shell -python client.py --partition-id 0 -``` - -Start client 2 in the second terminal: - -```shell -python client.py --partition-id 1 -``` +### Run with the Deployment Engine -You will see that PyTorch is starting a federated training. Look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) for a detailed explanation. +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-pytorch-lightning/client.py b/examples/quickstart-pytorch-lightning/client.py deleted file mode 100644 index 6e21259cc492..000000000000 --- a/examples/quickstart-pytorch-lightning/client.py +++ /dev/null @@ -1,80 +0,0 @@ -import argparse -from collections import OrderedDict - -import pytorch_lightning as pl -import torch -from datasets.utils.logging import disable_progress_bar - -import flwr as fl -import mnist - -disable_progress_bar() - - -class FlowerClient(fl.client.NumPyClient): - def __init__(self, model, train_loader, val_loader, test_loader): - self.model = model - self.train_loader = train_loader - self.val_loader = val_loader - self.test_loader = test_loader - - def get_parameters(self, config): - encoder_params = _get_parameters(self.model.encoder) - decoder_params = _get_parameters(self.model.decoder) - return encoder_params + decoder_params - - def set_parameters(self, parameters): - _set_parameters(self.model.encoder, parameters[:4]) - _set_parameters(self.model.decoder, parameters[4:]) - - def fit(self, parameters, config): - self.set_parameters(parameters) - - trainer = pl.Trainer(max_epochs=1) - trainer.fit(self.model, self.train_loader, self.val_loader) - - return self.get_parameters(config={}), 55000, {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - - trainer = pl.Trainer() - results = trainer.test(self.model, self.test_loader) - loss = results[0]["test_loss"] - - return loss, 10000, {"loss": loss} - - -def _get_parameters(model): - return [val.cpu().numpy() for _, val in model.state_dict().items()] - - -def _set_parameters(model, parameters): - params_dict = zip(model.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - model.load_state_dict(state_dict, strict=True) - - -def main() -> None: - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--partition-id", - type=int, - choices=range(0, 10), - required=True, - help="Specifies the artificial data partition", - ) - args = parser.parse_args() - partition_id = args.partition_id - - # Model and data - model = mnist.LitAutoEncoder() - train_loader, val_loader, test_loader = mnist.load_data(partition_id) - - # Flower client - client = FlowerClient(model, train_loader, val_loader, test_loader).to_client() - fl.client.start_client(server_address="127.0.0.1:8080", client=client) - - -if __name__ == "__main__": - main() diff --git a/examples/quickstart-pytorch-lightning/pyproject.toml b/examples/quickstart-pytorch-lightning/pyproject.toml index a09aaa3d65b5..482fc1356527 100644 --- a/examples/quickstart-pytorch-lightning/pyproject.toml +++ b/examples/quickstart-pytorch-lightning/pyproject.toml @@ -1,17 +1,37 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "quickstart-pytorch-lightning" -version = "0.1.0" -description = "Federated Learning Quickstart with Flower and PyTorch Lightning" -authors = ["The Flower Authors "] +[project] +name = "pytorchlightning_example" +version = "1.0.0" +description = "Federated Learning with PyTorch Lightning and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "pytorch-lightning<2.0.0; sys_platform == 'darwin'", + "pytorch-lightning==1.6.0; sys_platform != 'darwin'", + "torch==1.13.1", + "torchvision==0.14.1", +] -[tool.poetry.dependencies] -python = "^3.8" -flwr = ">=1.0,<2.0" -# flwr = { path = "../../", develop = true } # Development -flwr-datasets = { extras = ["vision"], version = ">=0.0.2,<1.0.0" } -pytorch-lightning = "1.6.0" -torchvision = "0.14.1" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "pytorchlightning_example.server_app:app" +clientapp = "pytorchlightning_example.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +max-epochs = 1 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 4 diff --git a/examples/quickstart-pytorch-lightning/pytorchlightning_example/__init__.py b/examples/quickstart-pytorch-lightning/pytorchlightning_example/__init__.py new file mode 100644 index 000000000000..d08f79080230 --- /dev/null +++ b/examples/quickstart-pytorch-lightning/pytorchlightning_example/__init__.py @@ -0,0 +1 @@ +"""pytorchlightning_example: A Flower / PyTorch Lightning app.""" diff --git a/examples/quickstart-pytorch-lightning/pytorchlightning_example/client_app.py b/examples/quickstart-pytorch-lightning/pytorchlightning_example/client_app.py new file mode 100644 index 000000000000..394a98b1ee71 --- /dev/null +++ b/examples/quickstart-pytorch-lightning/pytorchlightning_example/client_app.py @@ -0,0 +1,59 @@ +"""pytorchlightning_example: A Flower / PyTorch Lightning app.""" + +import pytorch_lightning as pl +from datasets.utils.logging import disable_progress_bar +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context + +disable_progress_bar() + +from pytorchlightning_example.task import ( + LitAutoEncoder, + get_parameters, + load_data, + set_parameters, +) + + +class FlowerClient(NumPyClient): + def __init__(self, train_loader, val_loader, test_loader, max_epochs): + self.model = LitAutoEncoder() + self.train_loader = train_loader + self.val_loader = val_loader + self.test_loader = test_loader + self.max_epochs = max_epochs + + def fit(self, parameters, config): + """Train the model with data of this client.""" + set_parameters(self.model, parameters) + + trainer = pl.Trainer(max_epochs=self.max_epochs, enable_progress_bar=False) + trainer.fit(self.model, self.train_loader, self.val_loader) + + return get_parameters(self.model), len(self.train_loader.dataset), {} + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_parameters(self.model, parameters) + + trainer = pl.Trainer(enable_progress_bar=False) + results = trainer.test(self.model, self.test_loader) + loss = results[0]["test_loss"] + + return loss, len(self.test_loader.dataset), {} + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + train_loader, val_loader, test_loader = load_data(partition_id, num_partitions) + + # Read run_config to fetch hyperparameters relevant to this run + max_epochs = context.run_config["max-epochs"] + return FlowerClient(train_loader, val_loader, test_loader, max_epochs).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py b/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py new file mode 100644 index 000000000000..f89aa8bb79e4 --- /dev/null +++ b/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py @@ -0,0 +1,31 @@ +"""pytorchlightning_example: A Flower / PyTorch Lightning app.""" + +from flwr.common import Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from pytorchlightning_example.task import LitAutoEncoder, get_parameters + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components for ServerApp.""" + + # Convert model parameters to flwr.common.Parameters + ndarrays = get_parameters(LitAutoEncoder()) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define strategy + strategy = FedAvg( + fraction_fit=0.5, + fraction_evaluate=0.5, + initial_parameters=global_model_init, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-pytorch-lightning/mnist.py b/examples/quickstart-pytorch-lightning/pytorchlightning_example/task.py similarity index 56% rename from examples/quickstart-pytorch-lightning/mnist.py rename to examples/quickstart-pytorch-lightning/pytorchlightning_example/task.py index 2f6100fe94cc..5ec61859424d 100644 --- a/examples/quickstart-pytorch-lightning/mnist.py +++ b/examples/quickstart-pytorch-lightning/pytorchlightning_example/task.py @@ -1,19 +1,24 @@ -"""Adapted from the PyTorch Lightning quickstart example. +"""pytorchlightning_example: A Flower / PyTorch Lightning app.""" -Source: pytorchlightning.ai (2021/02/04) -""" +import logging +from collections import OrderedDict +from typing import Any -from flwr_datasets import FederatedDataset import pytorch_lightning as pl import torch +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner from torch import nn from torch.nn import functional as F +from torch.optim.adam import Adam from torch.utils.data import DataLoader from torchvision import transforms +logging.getLogger("pytorch_lightning").setLevel(logging.WARNING) + class LitAutoEncoder(pl.LightningModule): - def __init__(self): + def __init__(self) -> None: super().__init__() self.encoder = nn.Sequential( nn.Linear(28 * 28, 64), @@ -26,16 +31,16 @@ def __init__(self): nn.Linear(64, 28 * 28), ) - def forward(self, x): + def forward(self, x) -> Any: embedding = self.encoder(x) return embedding - def configure_optimizers(self): + def configure_optimizers(self) -> Adam: optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) return optimizer - def training_step(self, train_batch, batch_idx): - x, y = train_batch + def training_step(self, train_batch, batch_idx) -> torch.Tensor: + x = train_batch["image"] x = x.view(x.size(0), -1) z = self.encoder(x) x_hat = self.decoder(z) @@ -43,14 +48,14 @@ def training_step(self, train_batch, batch_idx): self.log("train_loss", loss) return loss - def validation_step(self, batch, batch_idx): + def validation_step(self, batch, batch_idx) -> None: self._evaluate(batch, "val") - def test_step(self, batch, batch_idx): + def test_step(self, batch, batch_idx) -> None: self._evaluate(batch, "test") - def _evaluate(self, batch, stage=None): - x, y = batch + def _evaluate(self, batch, stage=None) -> None: + x = batch["image"] x = x.view(x.size(0), -1) z = self.encoder(x) x_hat = self.decoder(z) @@ -59,15 +64,14 @@ def _evaluate(self, batch, stage=None): self.log(f"{stage}_loss", loss, prog_bar=True) -def collate_fn(batch): - """Change the dictionary to tuple to keep the exact dataloader behavior.""" - images = [item["image"] for item in batch] - labels = [item["label"] for item in batch] +def get_parameters(model): + return [val.cpu().numpy() for _, val in model.state_dict().items()] - images_tensor = torch.stack(images) - labels_tensor = torch.tensor(labels) - return images_tensor, labels_tensor +def set_parameters(model, parameters): + params_dict = zip(model.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) def apply_transforms(batch): @@ -76,9 +80,19 @@ def apply_transforms(batch): return batch -def load_data(partition): - fds = FederatedDataset(dataset="mnist", partitioners={"train": 10}) - partition = fds.load_partition(partition, "train") +fds = None # Cache FederatedDataset + + +def load_data(partition_id, num_partitions): + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id, "train") partition = partition.with_transform(apply_transforms) # 20 % for on federated evaluation @@ -91,37 +105,12 @@ def load_data(partition): partition_train_valid["train"], batch_size=32, shuffle=True, - collate_fn=collate_fn, - num_workers=1, + num_workers=2, ) valloader = DataLoader( partition_train_valid["test"], batch_size=32, - collate_fn=collate_fn, - num_workers=1, - ) - testloader = DataLoader( - partition_full["test"], batch_size=32, collate_fn=collate_fn, num_workers=1 + num_workers=2, ) + testloader = DataLoader(partition_full["test"], batch_size=32, num_workers=1) return trainloader, valloader, testloader - - -def main() -> None: - """Centralized training.""" - - # Load data - train_loader, val_loader, test_loader = load_data(0) - - # Load model - model = LitAutoEncoder() - - # Train - trainer = pl.Trainer(max_epochs=5) - trainer.fit(model, train_loader, val_loader) - - # Test - trainer.test(model, test_loader) - - -if __name__ == "__main__": - main() diff --git a/examples/quickstart-pytorch-lightning/requirements.txt b/examples/quickstart-pytorch-lightning/requirements.txt deleted file mode 100644 index 6530dcc8c52c..000000000000 --- a/examples/quickstart-pytorch-lightning/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr>=1.0, <2.0 -flwr-datasets[vision]>=0.0.2, <1.0.0 -pytorch_lightning>=1.4.7 -torchvision==0.14.1 diff --git a/examples/quickstart-pytorch-lightning/run.sh b/examples/quickstart-pytorch-lightning/run.sh deleted file mode 100755 index 62a1dac199bd..000000000000 --- a/examples/quickstart-pytorch-lightning/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in $(seq 0 1); do - echo "Starting client $i" - python client.py --partition-id "${i}" & -done - -# This will allow you to use CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-pytorch-lightning/server.py b/examples/quickstart-pytorch-lightning/server.py deleted file mode 100644 index a104a1fffd26..000000000000 --- a/examples/quickstart-pytorch-lightning/server.py +++ /dev/null @@ -1,20 +0,0 @@ -import flwr as fl - - -def main() -> None: - # Define strategy - strategy = fl.server.strategy.FedAvg( - fraction_fit=0.5, - fraction_evaluate=0.5, - ) - - # Start Flower server for three rounds of federated learning - fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, - ) - - -if __name__ == "__main__": - main() From 05516325a0cccb2c2700dcb5117f8e358a9ccb74 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 7 Aug 2024 01:32:03 +0200 Subject: [PATCH 028/188] refactor(framework:skip) Rename FAB proto attribute (#3957) Co-authored-by: Daniel J. Beutel --- src/proto/flwr/proto/fab.proto | 4 ++-- src/py/flwr/proto/fab_pb2.py | 12 ++++++------ src/py/flwr/proto/fab_pb2.pyi | 16 ++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/proto/flwr/proto/fab.proto b/src/proto/flwr/proto/fab.proto index 1e796a59c82a..3620a95ff009 100644 --- a/src/proto/flwr/proto/fab.proto +++ b/src/proto/flwr/proto/fab.proto @@ -6,10 +6,10 @@ message Fab { // This field is the hash of the data field. It is used to identify the data. // The hash is calculated using the SHA-256 algorithm and is represented as a // hex string (sha256hex). - string hash = 1; + string hash_str = 1; // This field contains the fab file contents a one bytes blob. bytes content = 2; } -message GetFabRequest { string hash = 1; } +message GetFabRequest { string hash_str = 1; } message GetFabResponse { Fab fab = 1; } diff --git a/src/py/flwr/proto/fab_pb2.py b/src/py/flwr/proto/fab_pb2.py index c146a1635597..3f04e6693ab8 100644 --- a/src/py/flwr/proto/fab_pb2.py +++ b/src/py/flwr/proto/fab_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x66lwr/proto/fab.proto\x12\nflwr.proto\"$\n\x03\x46\x61\x62\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"\x1d\n\rGetFabRequest\x12\x0c\n\x04hash\x18\x01 \x01(\t\".\n\x0eGetFabResponse\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fabb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x66lwr/proto/fab.proto\x12\nflwr.proto\"(\n\x03\x46\x61\x62\x12\x10\n\x08hash_str\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"!\n\rGetFabRequest\x12\x10\n\x08hash_str\x18\x01 \x01(\t\".\n\x0eGetFabResponse\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fabb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -22,9 +22,9 @@ if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _globals['_FAB']._serialized_start=36 - _globals['_FAB']._serialized_end=72 - _globals['_GETFABREQUEST']._serialized_start=74 - _globals['_GETFABREQUEST']._serialized_end=103 - _globals['_GETFABRESPONSE']._serialized_start=105 - _globals['_GETFABRESPONSE']._serialized_end=151 + _globals['_FAB']._serialized_end=76 + _globals['_GETFABREQUEST']._serialized_start=78 + _globals['_GETFABREQUEST']._serialized_end=111 + _globals['_GETFABRESPONSE']._serialized_start=113 + _globals['_GETFABRESPONSE']._serialized_end=159 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/fab_pb2.pyi b/src/py/flwr/proto/fab_pb2.pyi index dafc217d0ce2..b2715dde5021 100644 --- a/src/py/flwr/proto/fab_pb2.pyi +++ b/src/py/flwr/proto/fab_pb2.pyi @@ -12,9 +12,9 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class Fab(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - HASH_FIELD_NUMBER: builtins.int + HASH_STR_FIELD_NUMBER: builtins.int CONTENT_FIELD_NUMBER: builtins.int - hash: typing.Text + hash_str: typing.Text """This field is the hash of the data field. It is used to identify the data. The hash is calculated using the SHA-256 algorithm and is represented as a hex string (sha256hex). @@ -25,21 +25,21 @@ class Fab(google.protobuf.message.Message): def __init__(self, *, - hash: typing.Text = ..., + hash_str: typing.Text = ..., content: builtins.bytes = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["content",b"content","hash",b"hash"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["content",b"content","hash_str",b"hash_str"]) -> None: ... global___Fab = Fab class GetFabRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - HASH_FIELD_NUMBER: builtins.int - hash: typing.Text + HASH_STR_FIELD_NUMBER: builtins.int + hash_str: typing.Text def __init__(self, *, - hash: typing.Text = ..., + hash_str: typing.Text = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["hash",b"hash"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["hash_str",b"hash_str"]) -> None: ... global___GetFabRequest = GetFabRequest class GetFabResponse(google.protobuf.message.Message): From 7c5aaf377a961a86a536eb536e0e17e52e8c30e4 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 7 Aug 2024 01:37:39 +0200 Subject: [PATCH 029/188] feat(framework) Add FAB type (#3944) Co-authored-by: Daniel J. Beutel --- src/py/flwr/common/typing.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/py/flwr/common/typing.py b/src/py/flwr/common/typing.py index c050fe6d4a13..0a48ab98059c 100644 --- a/src/py/flwr/common/typing.py +++ b/src/py/flwr/common/typing.py @@ -199,3 +199,11 @@ class Run: fab_id: str fab_version: str override_config: UserConfig + + +@dataclass +class Fab: + """Fab file representation.""" + + hash_str: str + content: bytes From 6620182ad50cd13d9981c60d88606f577d45982e Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 7 Aug 2024 00:59:08 +0100 Subject: [PATCH 030/188] refactor(framework) Stop passing working directory to simulation backends (#3954) --- .../superlink/fleet/vce/backend/backend.py | 2 +- .../superlink/fleet/vce/backend/raybackend.py | 39 ++---------- .../fleet/vce/backend/raybackend_test.py | 60 ++----------------- .../server/superlink/fleet/vce/vce_api.py | 2 +- .../superlink/fleet/vce/vce_api_test.py | 30 +++++++++- 5 files changed, 39 insertions(+), 94 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/backend.py b/src/py/flwr/server/superlink/fleet/vce/backend/backend.py index 31c64bd3b233..56f31f22eb47 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/backend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/backend.py @@ -29,7 +29,7 @@ class Backend(ABC): """Abstract base class for a Simulation Engine Backend.""" - def __init__(self, backend_config: BackendConfig, work_dir: str) -> None: + def __init__(self, backend_config: BackendConfig) -> None: """Construct a backend.""" @abstractmethod diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py index 0ab29a234f88..2087a88360c4 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py @@ -14,9 +14,8 @@ # ============================================================================== """Ray backend for the Fleet API using the Simulation Engine.""" -import pathlib from logging import DEBUG, ERROR -from typing import Callable, Dict, List, Tuple, Union +from typing import Callable, Dict, Tuple, Union import ray @@ -33,7 +32,6 @@ ClientResourcesDict = Dict[str, Union[int, float]] ActorArgsDict = Dict[str, Union[int, float, Callable[[], None]]] -RunTimeEnvDict = Dict[str, Union[str, List[str]]] class RayBackend(Backend): @@ -42,18 +40,14 @@ class RayBackend(Backend): def __init__( self, backend_config: BackendConfig, - work_dir: str, ) -> None: """Prepare RayBackend by initialising Ray and creating the ActorPool.""" log(DEBUG, "Initialising: %s", self.__class__.__name__) log(DEBUG, "Backend config: %s", backend_config) - if not pathlib.Path(work_dir).exists(): - raise ValueError(f"Specified work_dir {work_dir} does not exist.") - # Initialise ray self.init_args_key = "init_args" - self.init_ray(backend_config, work_dir) + self.init_ray(backend_config) # Validate client resources self.client_resources_key = "client_resources" @@ -68,23 +62,6 @@ def __init__( actor_kwargs=actor_kwargs, ) - def _configure_runtime_env(self, work_dir: str) -> RunTimeEnvDict: - """Return list of files/subdirectories to exclude relative to work_dir. - - Without this, Ray will push everything to the Ray Cluster. - """ - runtime_env: RunTimeEnvDict = {"working_dir": work_dir} - - excludes = [] - path = pathlib.Path(work_dir) - for p in path.rglob("*"): - # Exclude files need to be relative to the working_dir - if p.is_file() and not str(p).endswith(".py"): - excludes.append(str(p.relative_to(path))) - runtime_env["excludes"] = excludes - - return runtime_env - def _validate_client_resources(self, config: BackendConfig) -> ClientResourcesDict: client_resources_config = config.get(self.client_resources_key) client_resources: ClientResourcesDict = {} @@ -123,26 +100,18 @@ def _validate_actor_arguments(self, config: BackendConfig) -> ActorArgsDict: actor_args["on_actor_init_fn"] = enable_tf_gpu_growth return actor_args - def init_ray(self, backend_config: BackendConfig, work_dir: str) -> None: + def init_ray(self, backend_config: BackendConfig) -> None: """Intialises Ray if not already initialised.""" if not ray.is_initialized(): - # Init ray and append working dir if needed - runtime_env = ( - self._configure_runtime_env(work_dir=work_dir) if work_dir else None - ) - ray_init_args: Dict[ str, - Union[ConfigsRecordValues, RunTimeEnvDict], + ConfigsRecordValues, ] = {} if backend_config.get(self.init_args_key): for k, v in backend_config[self.init_args_key].items(): ray_init_args[k] = v - if runtime_env is not None: - ray_init_args["runtime_env"] = runtime_env - ray.init(**ray_init_args) @property diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py index a38cff96ceef..2f62bf725907 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py @@ -15,14 +15,13 @@ """Test for Ray backend for the Fleet API using the Simulation Engine.""" from math import pi -from pathlib import Path from typing import Callable, Dict, Optional, Tuple, Union from unittest import TestCase import ray from flwr.client import Client, NumPyClient -from flwr.client.client_app import ClientApp, LoadClientAppError +from flwr.client.client_app import ClientApp from flwr.client.node_state import NodeState from flwr.common import ( DEFAULT_TTL, @@ -36,7 +35,6 @@ Scalar, ) from flwr.common.constant import PARTITION_ID_KEY -from flwr.common.object_ref import load_app from flwr.common.recordset_compat import getpropertiesins_to_recordset from flwr.server.superlink.fleet.vce.backend.backend import BackendConfig from flwr.server.superlink.fleet.vce.backend.raybackend import RayBackend @@ -63,25 +61,6 @@ def _load_app() -> ClientApp: return ClientApp(client_fn=get_dummy_client) -client_app = ClientApp( - client_fn=get_dummy_client, -) - - -def _load_from_module(client_app_module_name: str) -> Callable[[], ClientApp]: - def _load_app() -> ClientApp: - app = load_app(client_app_module_name, LoadClientAppError) - - if not isinstance(app, ClientApp): - raise LoadClientAppError( - f"Attribute {client_app_module_name} is not of type {ClientApp}", - ) from None - - return app - - return _load_app - - def backend_build_process_and_termination( backend: RayBackend, process_args: Optional[Tuple[Callable[[], ClientApp], Message, Context]] = None, @@ -140,16 +119,15 @@ def doCleanups(self) -> None: def test_backend_creation_and_termination(self) -> None: """Test creation of RayBackend and its termination.""" - backend = RayBackend(backend_config={}, work_dir="") + backend = RayBackend(backend_config={}) backend_build_process_and_termination(backend=backend, process_args=None) def test_backend_creation_submit_and_termination( self, client_app_loader: Callable[[], ClientApp] = _load_app, - workdir: str = "", ) -> None: """Test submitting a message to a given ClientApp.""" - backend = RayBackend(backend_config={}, work_dir=workdir) + backend = RayBackend(backend_config={}) # Define ClientApp client_app_callable = client_app_loader @@ -178,42 +156,14 @@ def test_backend_creation_submit_and_termination( ] assert obtained_result_in_context == expected_output - def test_backend_creation_submit_and_termination_non_existing_client_app( - self, - ) -> None: - """Testing with ClientApp module that does not exist.""" - with self.assertRaises(LoadClientAppError): - self.test_backend_creation_submit_and_termination( - client_app_loader=_load_from_module("a_non_existing_module:app") - ) - def test_backend_creation_submit_and_termination_existing_client_app( self, ) -> None: """Testing with ClientApp module that exist.""" - # Resolve what should be the workdir to pass upon Backend initialisation - file_path = Path(__file__) - working_dir = Path.cwd() - rel_workdir = file_path.relative_to(working_dir) - - # Susbtract last element - rel_workdir_str = str(rel_workdir.parent) - self.test_backend_creation_submit_and_termination( - client_app_loader=_load_from_module("raybackend_test:client_app"), - workdir=rel_workdir_str, + client_app_loader=_load_app, ) - def test_backend_creation_submit_and_termination_existing_client_app_unsetworkdir( - self, - ) -> None: - """Testing with ClientApp module that exist but the passed workdir does not.""" - with self.assertRaises(ValueError): - self.test_backend_creation_submit_and_termination( - client_app_loader=_load_from_module("raybackend_test:client_app"), - workdir="/?&%$^#%@$!", - ) - def test_backend_creation_with_init_arguments(self) -> None: """Testing whether init args are properly parsed to Ray.""" backend_config_4: BackendConfig = { @@ -228,7 +178,6 @@ def test_backend_creation_with_init_arguments(self) -> None: RayBackend( backend_config=backend_config_4, - work_dir="", ) nodes = ray.nodes() @@ -238,7 +187,6 @@ def test_backend_creation_with_init_arguments(self) -> None: RayBackend( backend_config=backend_config_2, - work_dir="", ) nodes = ray.nodes() diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index f11576d63396..6216c49c4007 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -339,7 +339,7 @@ def start_vce( def backend_fn() -> Backend: """Instantiate a Backend.""" - return backend_type(backend_config, work_dir=app_dir) + return backend_type(backend_config) # Load ClientApp if needed def _load() -> ClientApp: diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py index 33c359af5cc8..70c8669ad883 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py @@ -26,13 +26,18 @@ from unittest import TestCase from uuid import UUID +from flwr.client import Client, ClientApp, NumPyClient from flwr.client.client_app import LoadClientAppError from flwr.common import ( DEFAULT_TTL, + Config, + ConfigsRecord, + Context, GetPropertiesIns, Message, MessageTypeLegacy, Metadata, + Scalar, ) from flwr.common.recordset_compat import getpropertiesins_to_recordset from flwr.common.serde import message_from_taskres, message_to_taskins @@ -45,6 +50,28 @@ from flwr.server.superlink.state import InMemoryState, StateFactory +class DummyClient(NumPyClient): + """A dummy NumPyClient for tests.""" + + def get_properties(self, config: Config) -> Dict[str, Scalar]: + """Return properties by doing a simple calculation.""" + result = float(config["factor"]) * pi + + # store something in context + self.context.state.configs_records["result"] = ConfigsRecord({"result": result}) + return {"result": result} + + +def get_dummy_client(context: Context) -> Client: # pylint: disable=unused-argument + """Return a DummyClient converted to Client type.""" + return DummyClient().to_client() + + +dummy_client_app = ClientApp( + client_fn=get_dummy_client, +) + + def terminate_simulation(f_stop: threading.Event, sleep_duration: int) -> None: """Set event to terminate Simulation Engine after `sleep_duration` seconds.""" sleep(sleep_duration) @@ -137,7 +164,7 @@ def _autoresolve_app_dir(rel_client_app_dir: str = "backend") -> str: # pylint: disable=too-many-arguments def start_and_shutdown( backend: str = "ray", - client_app_attr: str = "raybackend_test:client_app", + client_app_attr: Optional[str] = None, app_dir: str = "", num_supernodes: Optional[int] = None, state_factory: Optional[StateFactory] = None, @@ -169,6 +196,7 @@ def start_and_shutdown( start_vce( num_supernodes=num_supernodes, + client_app=None if client_app_attr else dummy_client_app, client_app_attr=client_app_attr, backend_name=backend, backend_config_json_stream=backend_config, From 1be146fb1012dc0ff93f0ab01a4d8cca3fb53991 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Wed, 7 Aug 2024 21:31:43 +0200 Subject: [PATCH 031/188] refactor(examples) Update secure aggregation example (#3833) Co-authored-by: Heng Pan Co-authored-by: jafermarq --- examples/app-secure-aggregation/README.md | 99 -------------- examples/app-secure-aggregation/client.py | 34 ----- .../app-secure-aggregation/pyproject.toml | 14 -- .../app-secure-aggregation/requirements.txt | 1 - examples/app-secure-aggregation/run.sh | 34 ----- examples/app-secure-aggregation/server.py | 45 ------ examples/flower-secure-aggregation/README.md | 72 ++++++++++ .../flower-secure-aggregation/pyproject.toml | 46 +++++++ .../secaggexample/__init__.py | 1 + .../secaggexample/client_app.py | 91 +++++++++++++ .../secaggexample/server_app.py | 81 +++++++++++ .../secaggexample/task.py | 128 ++++++++++++++++++ .../secaggexample}/workflow_with_log.py | 41 ++++-- 13 files changed, 445 insertions(+), 242 deletions(-) delete mode 100644 examples/app-secure-aggregation/README.md delete mode 100644 examples/app-secure-aggregation/client.py delete mode 100644 examples/app-secure-aggregation/pyproject.toml delete mode 100644 examples/app-secure-aggregation/requirements.txt delete mode 100755 examples/app-secure-aggregation/run.sh delete mode 100644 examples/app-secure-aggregation/server.py create mode 100644 examples/flower-secure-aggregation/README.md create mode 100644 examples/flower-secure-aggregation/pyproject.toml create mode 100644 examples/flower-secure-aggregation/secaggexample/__init__.py create mode 100644 examples/flower-secure-aggregation/secaggexample/client_app.py create mode 100644 examples/flower-secure-aggregation/secaggexample/server_app.py create mode 100644 examples/flower-secure-aggregation/secaggexample/task.py rename examples/{app-secure-aggregation => flower-secure-aggregation/secaggexample}/workflow_with_log.py (74%) diff --git a/examples/app-secure-aggregation/README.md b/examples/app-secure-aggregation/README.md deleted file mode 100644 index 8e483fb2f6bd..000000000000 --- a/examples/app-secure-aggregation/README.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -tags: [basic, vision, fds] -dataset: [] -framework: [numpy] ---- - -# Secure aggregation with Flower (the SecAgg+ protocol) πŸ§ͺ - -> πŸ§ͺ = This example covers experimental features that might change in future versions of Flower -> Please consult the regular PyTorch code examples ([quickstart](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch), [advanced](https://github.com/adap/flower/tree/main/examples/advanced-pytorch)) to learn how to use Flower with PyTorch. - -The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflow`. - -## Preconditions - -Let's assume the following project structure: - -```bash -$ tree . -. -β”œβ”€β”€ client.py # Client application using `secaggplus_mod` -β”œβ”€β”€ server.py # Server application using `SecAggPlusWorkflow` -β”œβ”€β”€ workflow_with_log.py # Augmented `SecAggPlusWorkflow` -β”œβ”€β”€ run.sh # Quick start script -β”œβ”€β”€ pyproject.toml # Project dependencies (poetry) -└── requirements.txt # Project dependencies (pip) -``` - -## Installing dependencies - -Project dependencies (such as and `flwr`) are defined in `pyproject.toml`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -### Poetry - -```shell -poetry install -poetry shell -``` - -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python3 -c "import flwr" -``` - -### pip - -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt -``` - -If you don't see any errors you're good to go! - -## Run the example with one command (recommended) - -```bash -./run.sh -``` - -## Run the example with the simulation engine - -```bash -flower-simulation --server-app server:app --client-app client:app --num-supernodes 5 -``` - -## Alternatively, run the example (in 7 terminal windows) - -Start the Flower Superlink in one terminal window: - -```bash -flower-superlink --insecure -``` - -Start 5 Flower `ClientApp` in 5 separate terminal windows: - -```bash -flower-client-app client:app --insecure -``` - -Start the Flower `ServerApp`: - -```bash -flower-server-app server:app --insecure --verbose -``` - -## Amend the example for practical usage - -For real-world applications, modify the `workflow` in `server.py` as follows: - -```python -workflow = fl.server.workflow.DefaultWorkflow( - fit_workflow=SecAggPlusWorkflow( - num_shares=, - reconstruction_threshold=, - ) -) -``` diff --git a/examples/app-secure-aggregation/client.py b/examples/app-secure-aggregation/client.py deleted file mode 100644 index b2fd02ec00d4..000000000000 --- a/examples/app-secure-aggregation/client.py +++ /dev/null @@ -1,34 +0,0 @@ -import time - -from flwr.client import ClientApp, NumPyClient -from flwr.client.mod import secaggplus_mod -import numpy as np - - -# Define FlowerClient and client_fn -class FlowerClient(NumPyClient): - def fit(self, parameters, config): - # Instead of training and returning model parameters, - # the client directly returns [1.0, 1.0, 1.0] for demonstration purposes. - ret_vec = [np.ones(3)] - # Force a significant delay for testing purposes - if "drop" in config and config["drop"]: - print(f"Client dropped for testing purposes.") - time.sleep(8) - else: - print(f"Client uploading {ret_vec[0]}...") - return ret_vec, 1, {} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - return FlowerClient().to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, - mods=[ - secaggplus_mod, - ], -) diff --git a/examples/app-secure-aggregation/pyproject.toml b/examples/app-secure-aggregation/pyproject.toml deleted file mode 100644 index fb1f636d8c33..000000000000 --- a/examples/app-secure-aggregation/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "app-secure-aggregation" -version = "0.1.0" -description = "Flower Secure Aggregation example." -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = "^3.8" -# Mandatory dependencies -flwr = { version = "^1.8.0", extras = ["simulation"] } diff --git a/examples/app-secure-aggregation/requirements.txt b/examples/app-secure-aggregation/requirements.txt deleted file mode 100644 index 2d8be098f264..000000000000 --- a/examples/app-secure-aggregation/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flwr[simulation]>=1.8.0 diff --git a/examples/app-secure-aggregation/run.sh b/examples/app-secure-aggregation/run.sh deleted file mode 100755 index fa8dc47f26ef..000000000000 --- a/examples/app-secure-aggregation/run.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Kill any currently running client.py processes -pkill -f 'flower-client-app' - -# Kill any currently running flower-superlink processes -pkill -f 'flower-superlink' - -# Start the flower server -echo "Starting flower server in background..." -flower-superlink --insecure > /dev/null 2>&1 & -sleep 2 - -# Number of client processes to start -N=5 # Replace with your desired value - -echo "Starting $N ClientApps in background..." - -# Start N client processes -for i in $(seq 1 $N) -do - flower-client-app --insecure client:app > /dev/null 2>&1 & - sleep 0.1 -done - -echo "Starting ServerApp..." -flower-server-app --insecure server:app --verbose - -echo "Clearing background processes..." - -# Kill any currently running client.py processes -pkill -f 'flower-client-app' - -# Kill any currently running flower-superlink processes -pkill -f 'flower-superlink' diff --git a/examples/app-secure-aggregation/server.py b/examples/app-secure-aggregation/server.py deleted file mode 100644 index ebd70045fdc5..000000000000 --- a/examples/app-secure-aggregation/server.py +++ /dev/null @@ -1,45 +0,0 @@ -from flwr.common import Context -from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig -from flwr.server.strategy import FedAvg -from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow - -from workflow_with_log import SecAggPlusWorkflowWithLogs - - -# Define strategy -strategy = FedAvg( - fraction_fit=1.0, # Select all available clients - fraction_evaluate=0.0, # Disable evaluation - min_available_clients=5, -) - - -# Flower ServerApp -app = ServerApp() - - -@app.main() -def main(driver: Driver, context: Context) -> None: - # Construct the LegacyContext - context = LegacyContext( - context=context, - config=ServerConfig(num_rounds=3), - strategy=strategy, - ) - - # Create the workflow - workflow = DefaultWorkflow( - fit_workflow=SecAggPlusWorkflowWithLogs( - num_shares=3, - reconstruction_threshold=2, - timeout=5, - ) - # # For real-world applications, use the following code instead - # fit_workflow=SecAggPlusWorkflow( - # num_shares=, - # reconstruction_threshold=, - # ) - ) - - # Execute - workflow(driver, context) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md new file mode 100644 index 000000000000..9e92aed01d9e --- /dev/null +++ b/examples/flower-secure-aggregation/README.md @@ -0,0 +1,72 @@ +--- +tags: [advanced, secure_aggregation, privacy] +dataset: [CIFAR-10] +framework: [torch, torchvision] +--- + +# Secure aggregation with Flower (the SecAgg+ protocol) + +The following steps describe how to use Flower's built-in Secure Aggregation components. This example demonstrates how to apply `SecAgg+` to the same federated learning workload as in the [quickstart-pytorch](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example. The `ServerApp` uses the [`SecAggPlusWorkflow`](https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html#secaggplusworkflow) while `ClientApp` uses the [`secaggplus_mod`](https://flower.ai/docs/framework/ref-api/flwr.client.mod.secaggplus_mod.html#flwr.client.mod.secaggplus_mod). To introduce the various steps involved in `SecAgg+`, this example introduces as a sub-class of `SecAggPlusWorkflow` the `SecAggPlusWorkflowWithLogs`. It is enabled by default, but you can disable (see later in this readme). + +## Set up the project + +### Clone the project + +Start by cloning the example project: + +```shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/flower-secure-aggregation . \ + && rm -rf _tmp && cd flower-secure-aggregation +``` + +This will create a new directory called `flower-secure-aggregation` containing the +following files: + +```shell +flower-secure-aggregation +| +β”œβ”€β”€ secaggexample +| β”œβ”€β”€ __init__.py +| β”œβ”€β”€ client_app.py # Defines your ClientApp +| β”œβ”€β”€ server_app.py # Defines your ServerApp +| β”œβ”€β”€ task.py # Defines your model, training and data loading +| └── workflow_with_log.py # Defines a workflow used when `is-demo=true` +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md +``` + +### Install dependencies and project + +Install the dependencies defined in `pyproject.toml` as well as the `secaggexample` package. + +```bash +pip install -e . +``` + +## Run the project + +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. + +### Run with the Simulation Engine + +```bash +flwr run . +``` + +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example + +```bash +flwr run . --run-config num-server-rounds=5,learning-rate=0.25 +``` + +To adapt the example for a practial usage, set `is-demo=false` like shown below. You might want to adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. You can override those via `--run-config` as well. + +```bash +flwr run . --run-config is-demo=false +``` + +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml new file mode 100644 index 000000000000..d9be719653b0 --- /dev/null +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "secaggexample" +version = "1.0.0" +description = "Secure Aggregation in Flower" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "secaggexample.server_app:app" +clientapp = "secaggexample.client_app:app" + + +[tool.flwr.app.config] +num-server-rounds = 3 +fraction-evaluate = 0.5 +local-epochs = 1 +learning-rate = 0.1 +batch-size = 32 +# Parameters for the SecAgg+ protocol +num-shares = 3 +reconstruction-threshold = 2 +max-weight = 9000 +timeout = 15.0 +# Demo flag +is-demo = true + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 5 diff --git a/examples/flower-secure-aggregation/secaggexample/__init__.py b/examples/flower-secure-aggregation/secaggexample/__init__.py new file mode 100644 index 000000000000..366ceebfae80 --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/__init__.py @@ -0,0 +1 @@ +"""secaggexample: A Flower with SecAgg+ app.""" diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py new file mode 100644 index 000000000000..7f4fd54b98b5 --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -0,0 +1,91 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + +import time + +import torch +from flwr.client import ClientApp, NumPyClient +from flwr.client.mod import secaggplus_mod +from flwr.common import Context + +from secaggexample.task import Net, get_weights, load_data, set_weights, test, train + + +# Define Flower Client +class FlowerClient(NumPyClient): + def __init__( + self, trainloader, valloader, local_epochs, learning_rate, timeout, is_demo + ): + self.net = Net() + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.lr = learning_rate + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + # For demonstration purposes only + self.timeout = timeout + self.is_demo = is_demo + + def fit(self, parameters, config): + """Train the model with data of this client.""" + set_weights(self.net, parameters) + results = {} + if not self.is_demo: + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.lr, + self.device, + ) + ret_vec = get_weights(self.net) + + # Force a significant delay for testing purposes + if self.is_demo: + if config.get("drop", False): + print(f"Client dropped for testing purposes.") + time.sleep(self.timeout) + else: + print(f"Client uploading parameters: {ret_vec[0].flatten()[:3]}...") + return ret_vec, len(self.trainloader.dataset), results + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_weights(self.net, parameters) + loss, accuracy = 0.0, 0.0 + if not self.is_demo: + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + # Read run_config to fetch hyperparameters relevant to this run + batch_size = context.run_config["batch-size"] + is_demo = context.run_config["is-demo"] + trainloader, valloader = load_data( + partition_id, num_partitions, batch_size, is_demo + ) + local_epochs = context.run_config["local-epochs"] + lr = context.run_config["learning-rate"] + # For demostrations purposes only + timeout = context.run_config["timeout"] + + # Return Client instance + return FlowerClient( + trainloader, valloader, local_epochs, lr, timeout, is_demo + ).to_client() + + +# Flower ClientApp +app = ClientApp( + client_fn=client_fn, + mods=[ + secaggplus_mod, + ], +) diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py new file mode 100644 index 000000000000..a332ffb9eca1 --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -0,0 +1,81 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + +from logging import DEBUG +from typing import List, Tuple + +from secaggexample.task import get_weights, make_net +from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs + +from flwr.common import Context, Metrics, ndarrays_to_parameters +from flwr.common.logger import update_console_handler + +from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig +from flwr.server.strategy import FedAvg +from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +# Flower ServerApp +app = ServerApp() + + +@app.main() +def main(driver: Driver, context: Context) -> None: + + is_demo = context.run_config["is-demo"] + + # Get initial parameters + ndarrays = get_weights(make_net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define strategy + strategy = FedAvg( + # Select all available clients + fraction_fit=1.0, + # Disable evaluation in demo + fraction_evaluate=(0.0 if is_demo else context.run_config["fraction-evaluate"]), + min_available_clients=5, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) + + # Construct the LegacyContext + num_rounds = context.run_config["num-server-rounds"] + context = LegacyContext( + context=context, + config=ServerConfig(num_rounds=num_rounds), + strategy=strategy, + ) + + # Create fit workflow + # For further information, please see: + # https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html + if is_demo: + update_console_handler(DEBUG, True, True) + fit_workflow = SecAggPlusWorkflowWithLogs( + num_shares=context.run_config["num-shares"], + reconstruction_threshold=context.run_config["reconstruction-threshold"], + max_weight=1, + timeout=context.run_config["timeout"], + ) + else: + fit_workflow = SecAggPlusWorkflow( + num_shares=context.run_config["num-shares"], + reconstruction_threshold=context.run_config["reconstruction-threshold"], + max_weight=context.run_config["max-weight"], + ) + + # Create the workflow + workflow = DefaultWorkflow(fit_workflow=fit_workflow) + + # Execute + workflow(driver, context) diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py new file mode 100644 index 000000000000..e9cca8ef9115 --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -0,0 +1,128 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + +import random +from collections import OrderedDict +from unittest.mock import Mock + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def make_net(seed=42): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + return Net() + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int, batch_size: int, is_demo: bool): + """Load partition CIFAR10 data.""" + if is_demo: + trainloader, testloader = Mock(dataset=[0]), Mock(dataset=[0]) + return trainloader, testloader + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader( + partition_train_test["train"], batch_size=batch_size, shuffle=True + ) + testloader = DataLoader(partition_train_test["test"], batch_size=batch_size) + return trainloader, testloader + + +def train(net, trainloader, valloader, epochs, learning_rate, device): + """Train the model on the training set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss().to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) + net.train() + for _ in range(epochs): + for batch in trainloader: + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + criterion(net(images.to(device)), labels.to(device)).backward() + optimizer.step() + + val_loss, val_acc = test(net, valloader, device) + + results = { + "val_loss": val_loss, + "val_accuracy": val_acc, + } + return results + + +def test(net, testloader, device): + """Validate the model on the test set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + loss = loss / len(testloader) + return loss, accuracy diff --git a/examples/app-secure-aggregation/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py similarity index 74% rename from examples/app-secure-aggregation/workflow_with_log.py rename to examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index a03ff8c13b6c..b2e457484de7 100644 --- a/examples/app-secure-aggregation/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,14 +1,18 @@ -from flwr.common import Context, log, parameters_to_ndarrays +"""secaggexample: A Flower with SecAgg+ app.""" + from logging import INFO + +from secaggexample.task import get_weights, make_net + +import flwr.common.recordset_compat as compat +from flwr.common import Context, log, parameters_to_ndarrays +from flwr.common.secure_aggregation.quantization import quantize from flwr.server import Driver, LegacyContext +from flwr.server.workflow.constant import MAIN_PARAMS_RECORD from flwr.server.workflow.secure_aggregation.secaggplus_workflow import ( SecAggPlusWorkflow, WorkflowState, ) -import numpy as np -from flwr.common.secure_aggregation.quantization import quantize -from flwr.server.workflow.constant import MAIN_PARAMS_RECORD -import flwr.common.recordset_compat as compat class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): @@ -21,8 +25,11 @@ class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): node_ids = [] def __call__(self, driver: Driver, context: Context) -> None: + first_3_params = get_weights(make_net())[0].flatten()[:3] _quantized = quantize( - [np.ones(3) for _ in range(5)], self.clipping_range, self.quantization_range + [first_3_params for _ in range(5)], + self.clipping_range, + self.quantization_range, ) log(INFO, "") log( @@ -31,24 +38,24 @@ def __call__(self, driver: Driver, context: Context) -> None: ) log( INFO, - "In the example, each client will upload a vector [1.0, 1.0, 1.0] instead of", + "In the example, clients will skip model training and evaluation", ) - log(INFO, "model updates for demonstration purposes.") + log(INFO, "for demonstration purposes.") log( INFO, "Client 0 is configured to drop out before uploading the masked vector.", ) log(INFO, "After quantization, the raw vectors will look like:") for i in range(1, 5): - log(INFO, "\t%s from Client %s", _quantized[i], i) + log(INFO, "\t%s... from Client %s", _quantized[i], i) log( INFO, - "Numbers are rounded to integers stochastically during the quantization", + "Numbers are rounded to integers stochastically during the quantization, ", ) - log(INFO, ", and thus entries may not be identical.") + log(INFO, "and thus vectors may not be identical.") log( INFO, - "The above raw vectors are hidden from the driver through adding masks.", + "The above raw vectors are hidden from the ServerApp through adding masks.", ) log(INFO, "") log( @@ -63,8 +70,8 @@ def __call__(self, driver: Driver, context: Context) -> None: ndarrays = parameters_to_ndarrays(parameters) log( INFO, - "Weighted average of vectors (dequantized): %s", - ndarrays[0], + "Weighted average of parameters (dequantized): %s...", + ndarrays[0].flatten()[:3], ) log( INFO, @@ -88,5 +95,9 @@ def collect_masked_vectors_stage( ret = super().collect_masked_vectors_stage(driver, context, state) for node_id in state.sampled_node_ids - state.active_node_ids: log(INFO, "Client %s dropped out.", self.node_ids.index(node_id)) - log(INFO, "Obtained sum of masked vectors: %s", state.aggregate_ndarrays[1]) + log( + INFO, + "Obtained sum of masked parameters: %s...", + state.aggregate_ndarrays[1].flatten()[:3], + ) return ret From 8a074d2e7b2e6e1c123c71b0b3972dd510896ce6 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 8 Aug 2024 01:47:44 +0100 Subject: [PATCH 032/188] feat(datasets) Add tests for iris dataset (#3967) --- datasets/flwr_datasets/federated_dataset_test.py | 3 ++- datasets/flwr_datasets/utils.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/datasets/flwr_datasets/federated_dataset_test.py b/datasets/flwr_datasets/federated_dataset_test.py index 24c59ce029f0..64d75a7a7a5a 100644 --- a/datasets/flwr_datasets/federated_dataset_test.py +++ b/datasets/flwr_datasets/federated_dataset_test.py @@ -48,9 +48,10 @@ ("zh-plus/tiny-imagenet", "valid", ""), ("Mike0307/MNIST-M", "test", ""), ("flwrlabs/usps", "test", ""), - # Text + # Tabular ("scikit-learn/adult-census-income", None, ""), ("jlh/uci-mushrooms", None, ""), + ("scikit-learn/iris", None, ""), # Mocked # #Image ("cifar100", "test", ""), diff --git a/datasets/flwr_datasets/utils.py b/datasets/flwr_datasets/utils.py index 6cea5bd14137..32904ded2861 100644 --- a/datasets/flwr_datasets/utils.py +++ b/datasets/flwr_datasets/utils.py @@ -47,6 +47,7 @@ "jlh/uci-mushrooms", "Mike0307/MNIST-M", "flwrlabs/usps", + "scikit-learn/iris", ] From 584b566ac1e8bc544ad788d15612783e56034d19 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Thu, 8 Aug 2024 14:08:52 +0200 Subject: [PATCH 033/188] refactor(examples) Update Flower in 30 minutes example (#3846) Co-authored-by: jafermarq --- examples/flower-in-30-minutes/tutorial.ipynb | 637 +++++++++++-------- 1 file changed, 378 insertions(+), 259 deletions(-) diff --git a/examples/flower-in-30-minutes/tutorial.ipynb b/examples/flower-in-30-minutes/tutorial.ipynb index 9f0c86a2507a..ed8d9a49dcd7 100644 --- a/examples/flower-in-30-minutes/tutorial.ipynb +++ b/examples/flower-in-30-minutes/tutorial.ipynb @@ -11,7 +11,9 @@ "\n", "πŸ§‘β€πŸ« This tutorial starts at zero and expects no familiarity with federated learning. Only a basic understanding of data science and Python programming is assumed. A minimal understanding of ML is not required but if you already know about it, nothing is stopping your from modifying this code as you see fit!\n", "\n", - "> Star Flower on [GitHub ⭐️](https://github.com/adap/flower) and join the Flower community on Slack to connect, ask questions, and get help: [Join Slack 🌼](https://flower.ai/join-slack/). We'd love to hear from you in the #introductions channel! And if anything is unclear, head over to the #questions channel.\n", + "> [Star Flower on GitHub](https://github.com/adap/flower) ⭐️ and join the Flower community on Flower Discuss and the Flower Slack to connect, ask questions, and get help:\n", + "> - [Join Flower Discuss](https://discuss.flower.ai/) We'd love to hear from you in the `Introduction` topic! If anything is unclear, post in `Flower Help - Beginners`.\n", + "> - [Join Flower Slack](https://flower.ai/join-slack) We'd love to hear from you in the `#introductions` channel! If anything is unclear, head over to the `#questions` channel.\n", "\n", "Let's get started!" ] @@ -50,8 +52,7 @@ "metadata": {}, "outputs": [], "source": [ - "# depending on your shell, you might need to add `\\` before `[` and `]`.\n", - "!pip install -q flwr[simulation]" + "!pip install -q \"flwr[simulation]\" flwr-datasets" ] }, { @@ -59,7 +60,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We will be using the _simulation_ model in Flower, which allows you to run a large number of clients without the overheads of manually managing devices. This is achieved via the `Virtual Client Engine`, the core component that runs [FL Simulations](https://flower.ai/docs/framework/how-to-run-simulations.html) with Flower. With simulation, you can dynamically scale your experiments whether you run the code on your laptop, a machine with a single GPU, a server with multiple GPUs os even on a cluster with multiple servers. The `Virtual Client Engine` handles everything transparently and it allows you to specify how many resources (e.g. CPU cores, GPU VRAM) should be assigned to each virtual client." + "We will be using the _simulation_ engine in Flower, which allows you to run a large number of clients without the overheads of manually managing devices. This is achieved via the `Simulation Engine`, the core component in Flower to run simulations efficiently." ] }, { @@ -69,9 +70,9 @@ "source": [ "## Install your ML framework\n", "\n", - "Flower is agnostic to your choice of ML Framework. Flower works with `PyTorch`, `Tensorflow`, `NumPy`, `πŸ€— Transformers`, `MXNet`, `JAX`, `scikit-learn`, `fastai`, `Pandas`. Flower also supports all major platforms: `iOS`, `Android` and plain `C++`. You can find a _quickstart- example for each of the above in the [Flower Repository](https://github.com/adap/flower/tree/main/examples) inside the `examples/` directory. And check the [Flower Documentation](https://flower.ai/docs/) for even more learning materials.\n", + "Flower is agnostic to your choice of ML Framework. Flower works with `PyTorch`, `Tensorflow`, `NumPy`, `πŸ€— Transformers`, `MLX`, `JAX`, `scikit-learn`, `fastai`, `Pandas`. Flower also supports all major platforms: `iOS`, `Android` and plain `C++`. You can find a _quickstart- example for each of the above in the [Flower Repository](https://github.com/adap/flower/tree/main/examples) inside the `examples/` directory. And check the [Flower Documentation](https://flower.ai/docs/) for even more learning materials.\n", "\n", - "In this tutorial we are going to use PyTorch, so let's install a recent version. In this tutorial we'll use a small model so using CPU only training will suffice (this will also prevent Colab from abruptly terminating your experiment if resource limits are exceeded)" + "In this tutorial we are going to use PyTorch, uncomment the line below if you haven't installed PyTorch in your system. In this tutorial we'll use a small model so using CPU only training will suffice." ] }, { @@ -87,7 +88,7 @@ "source": [ "# you might see a warning after running the command below, this can be ignored\n", "# if you are running this outside Colab, you probably need to adjust the command below\n", - "!pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu" + "# !pip install torch==1.13.1+cpu torchvision==0.14.1+cpu --extra-index-url https://download.pytorch.org/whl/cpu" ] }, { @@ -132,7 +133,7 @@ "\n", "## A dataset\n", "\n", - "Let's begin by constructing the dataset." + "Let's begin by constructing the dataset. We will use πŸ€—HuggingFace Datasets to download MNIST. We will prepare a function that will be use later to apply standard normalization transformations from `TorchVision` and create the dataloaders for the `train` and `test` partitions." ] }, { @@ -145,39 +146,28 @@ "import torch\n", "from torch.utils.data import DataLoader\n", "from torchvision.transforms import ToTensor, Normalize, Compose\n", - "from torchvision.datasets import MNIST\n", + "from datasets import load_dataset\n", "\n", "\n", - "def get_mnist(data_path: str = \"./data\"):\n", - " \"\"\"This function downloads the MNIST dataset into the `data_path`\n", - " directory if it is not there already. WE construct the train/test\n", - " split by converting the images into tensors and normalising them\"\"\"\n", + "def get_mnist_dataloaders(mnist_dataset, batch_size: int):\n", + " pytorch_transforms = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))])\n", "\n", - " # transformation to convert images to tensors and apply normalisation\n", - " tr = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))])\n", + " # Prepare transformation functions\n", + " def apply_transforms(batch):\n", + " batch[\"image\"] = [pytorch_transforms(img) for img in batch[\"image\"]]\n", + " return batch\n", "\n", - " # prepare train and test set\n", - " trainset = MNIST(data_path, train=True, download=True, transform=tr)\n", - " testset = MNIST(data_path, train=False, download=True, transform=tr)\n", + " mnist_train = mnist_dataset[\"train\"].with_transform(apply_transforms)\n", + " mnist_test = mnist_dataset[\"test\"].with_transform(apply_transforms)\n", "\n", - " return trainset, testset" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's run the code above and do some visualisations to understand better the data we are working with !" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "trainset, testset = get_mnist()" + " # Construct PyTorch dataloaders\n", + " trainloader = DataLoader(mnist_train, batch_size=batch_size, shuffle=True)\n", + " testloader = DataLoader(mnist_test, batch_size=batch_size)\n", + " return trainloader, testloader\n", + "\n", + "\n", + "# Download dataset\n", + "mnist = load_dataset(\"ylecun/mnist\")" ] }, { @@ -185,7 +175,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can have a quick overview of our datasets by just typing the object on the command line. For instance, below you can see that the `trainset` has 60k training examples and will use the transformation rule we defined above in `get_mnist()`." + "We can have a quick overview of our datasets by just typing the object on the command line. For instance, below you can see the sizes of both the `train` and `test` partitions" ] }, { @@ -199,7 +189,7 @@ }, "outputs": [], "source": [ - "trainset" + "mnist" ] }, { @@ -223,21 +213,19 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", + "from collections import Counter\n", "\n", "\n", "# construct histogram\n", - "all_labels = trainset.targets\n", - "num_possible_labels = len(\n", - " set(all_labels.numpy().tolist())\n", - ") # this counts unique labels (so it should be = 10)\n", - "plt.hist(all_labels, bins=num_possible_labels)\n", + "all_labels = mnist[\"train\"][\"label\"]\n", + "all_label_counts = Counter(all_labels)\n", + "\n", + "# visualise histogram\n", + "bar = plt.bar(all_label_counts.keys(), all_label_counts.values())\n", + "_ = plt.bar_label(bar)\n", "\n", "# plot formatting\n", - "plt.xticks(range(num_possible_labels))\n", - "plt.grid()\n", - "plt.xlabel(\"Label\")\n", - "plt.ylabel(\"Number of images\")\n", - "plt.title(\"Class labels distribution for MNIST\")" + "_ = plt.xticks([label for label in all_label_counts.keys()])" ] }, { @@ -256,11 +244,15 @@ "source": [ "import random\n", "import numpy as np\n", + "from PIL import Image\n", + "import io\n", "\n", "\n", "def visualise_n_random_examples(trainset_, n: int, verbose: bool = True):\n", - " # take n examples at random\n", - " idx = list(range(len(trainset_.data)))\n", + " trainset_data = [\n", + " Image.open(io.BytesIO(entry[0].as_py())) for entry in trainset_.data[0]\n", + " ]\n", + " idx = list(range(len(trainset_data)))\n", " random.shuffle(idx)\n", " idx = idx[:n]\n", " if verbose:\n", @@ -273,7 +265,7 @@ "\n", " # display images on canvas\n", " for c_i, i in enumerate(idx):\n", - " axs.flat[c_i].imshow(trainset_.data[i], cmap=\"gray\")" + " axs.flat[c_i].imshow(trainset_data[i], cmap=\"gray\")" ] }, { @@ -290,7 +282,7 @@ "source": [ "# it is likely that the plot this function will generate looks familiar to other plots you might have generated before\n", "# or you might have encountered in other tutorials. So far, we aren't doing anything new, Federated Learning will start soon!\n", - "visualise_n_random_examples(trainset, n=32)" + "visualise_n_random_examples(mnist[\"train\"], n=32)" ] }, { @@ -383,13 +375,12 @@ " \"\"\"Train the network on the training set.\"\"\"\n", " criterion = torch.nn.CrossEntropyLoss()\n", " net.train()\n", - " for _ in range(epochs):\n", - " for images, labels in trainloader:\n", - " optimizer.zero_grad()\n", - " loss = criterion(net(images), labels)\n", - " loss.backward()\n", - " optimizer.step()\n", - " return net\n", + " for batch in trainloader:\n", + " images, labels = batch[\"image\"], batch[\"label\"]\n", + " optimizer.zero_grad()\n", + " loss = criterion(net(images), labels)\n", + " loss.backward()\n", + " optimizer.step()\n", "\n", "\n", "def test(net, testloader):\n", @@ -398,7 +389,8 @@ " correct, loss = 0, 0.0\n", " net.eval()\n", " with torch.no_grad():\n", - " for images, labels in testloader:\n", + " for batch in testloader:\n", + " images, labels = batch[\"image\"], batch[\"label\"]\n", " outputs = net(images)\n", " loss += criterion(outputs, labels).item()\n", " _, predicted = torch.max(outputs.data, 1)\n", @@ -407,7 +399,9 @@ " return loss, accuracy\n", "\n", "\n", - "def run_centralised(epochs: int, lr: float, momentum: float = 0.9):\n", + "def run_centralised(\n", + " trainloader, testloader, epochs: int, lr: float, momentum: float = 0.9\n", + "):\n", " \"\"\"A minimal (but complete) training loop\"\"\"\n", "\n", " # instantiate the model\n", @@ -416,16 +410,13 @@ " # define optimiser with hyperparameters supplied\n", " optim = torch.optim.SGD(model.parameters(), lr=lr, momentum=momentum)\n", "\n", - " # get dataset and construct a dataloaders\n", - " trainset, testset = get_mnist()\n", - " trainloader = DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)\n", - " testloader = DataLoader(testset, batch_size=128)\n", - "\n", " # train for the specified number of epochs\n", - " trained_model = train(model, trainloader, optim, epochs)\n", + " for e in range(epochs):\n", + " print(f\"Training epoch {e} ...\")\n", + " train(model, trainloader, optim, epochs)\n", "\n", " # training is completed, then evaluate model on the test set\n", - " loss, accuracy = test(trained_model, testloader)\n", + " loss, accuracy = test(model, testloader)\n", " print(f\"{loss = }\")\n", " print(f\"{accuracy = }\")" ] @@ -449,7 +440,11 @@ }, "outputs": [], "source": [ - "run_centralised(epochs=5, lr=0.01)" + "# Construct dataloaders\n", + "trainloader, testloader = get_mnist_dataloaders(mnist, batch_size=32)\n", + "\n", + "# Run the centralised training\n", + "run_centralised(trainloader, testloader, epochs=3, lr=0.01)" ] }, { @@ -477,7 +472,7 @@ "source": [ "## One Client, One Data Partition\n", "\n", - "To start designing a Federated Learning pipeline we need to meet one of the key properties in FL: each client has its own data partition. To accomplish this with the MNIST dataset, we are going to generate N random partitions, where N is the total number of clients in our FL system." + "To start designing a Federated Learning pipeline we need to meet one of the key properties in FL: each client has its own data partition. To accomplish this with the MNIST dataset, we are going to generate N random partitions, where N is the total number of clients in our FL system, using [Flower Datasets](https://flower.ai/docs/datasets/). Let's create 100 partitions with the [IidPartitioner](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.IidPartitioner.html#flwr_datasets.partitioner.IidPartitioner) -- note there are many more [partitioners](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.html) to choose from." ] }, { @@ -486,92 +481,63 @@ "metadata": {}, "outputs": [], "source": [ - "from torch.utils.data import random_split\n", - "\n", - "\n", - "def prepare_dataset(num_partitions: int, batch_size: int, val_ratio: float = 0.1):\n", - " \"\"\"This function partitions the training set into N disjoint\n", - " subsets, each will become the local dataset of a client. This\n", - " function also subsequently partitions each traininset partition\n", - " into train and validation. The test set is left intact and will\n", - " be used by the central server to asses the performance of the\n", - " global model.\"\"\"\n", + "from flwr_datasets import FederatedDataset\n", + "from flwr_datasets.partitioner import IidPartitioner\n", "\n", - " # get the MNIST dataset\n", - " trainset, testset = get_mnist()\n", + "NUM_PARTITIONS = 100\n", "\n", - " # split trainset into `num_partitions` trainsets\n", - " num_images = len(trainset) // num_partitions\n", - "\n", - " partition_len = [num_images] * num_partitions\n", - "\n", - " trainsets = random_split(\n", - " trainset, partition_len, torch.Generator().manual_seed(2023)\n", - " )\n", - "\n", - " # create dataloaders with train+val support\n", - " trainloaders = []\n", - " valloaders = []\n", - " for trainset_ in trainsets:\n", - " num_total = len(trainset_)\n", - " num_val = int(val_ratio * num_total)\n", - " num_train = num_total - num_val\n", - "\n", - " for_train, for_val = random_split(\n", - " trainset_, [num_train, num_val], torch.Generator().manual_seed(2023)\n", - " )\n", - "\n", - " trainloaders.append(\n", - " DataLoader(for_train, batch_size=batch_size, shuffle=True, num_workers=2)\n", - " )\n", - " valloaders.append(\n", - " DataLoader(for_val, batch_size=batch_size, shuffle=False, num_workers=2)\n", - " )\n", - "\n", - " # create dataloader for the test set\n", - " testloader = DataLoader(testset, batch_size=128)\n", - "\n", - " return trainloaders, valloaders, testloader" + "partitioner = IidPartitioner(num_partitions=NUM_PARTITIONS)\n", + "# Let's partition the \"train\" split of the MNIST dataset\n", + "# The MNIST dataset will be downloaded if it hasn't been already\n", + "fds = FederatedDataset(dataset=\"ylecun/mnist\", partitioners={\"train\": partitioner})" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Let's create 100 partitions and extract some statistics from one partition\n" + "Accessing individual partitions can be done like this. The return object can be then passed to a dataloader for training or evaluation." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 508 - }, - "outputId": "0f53ca81-cb55-46ef-c8e0-4e19a4f060b2" - }, + "metadata": {}, "outputs": [], "source": [ - "trainloaders, valloaders, testloader = prepare_dataset(\n", - " num_partitions=100, batch_size=32\n", - ")\n", - "\n", - "# first partition\n", - "train_partition = trainloaders[0].dataset\n", - "\n", - "# count data points\n", - "partition_indices = train_partition.indices\n", - "print(f\"number of images: {len(partition_indices)}\")\n", + "# We could load a single partition like this\n", + "partition_0 = fds.load_partition(0)\n", + "partition_0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Flower Datasets` comes with built-in visualization tools that help you get insights of how the dataset (in this case MNIST) has been partitioned. Let's create a parplot to visualize the number of labels of each class that every client's partition contains. Note we are only visualising the first 30 clients purely so the plot remain readable. \n", "\n", - "# visualise histogram\n", - "plt.hist(train_partition.dataset.dataset.targets[partition_indices], bins=10)\n", - "plt.grid()\n", - "plt.xticks(range(10))\n", - "plt.xlabel(\"Label\")\n", - "plt.ylabel(\"Number of images\")\n", - "plt.title(\"Class labels distribution for MNIST\")" + "> There are many more types of plots you can generated with Flower Datasets. Check the [Visualization tutorial](https://flower.ai/docs/datasets/tutorial-visualize-label-distribution.html). Feel free to try other partitioning scheemes and you'll see how the visualization changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from flwr_datasets.visualization import plot_label_distributions\n", + "\n", + "fig, ax, df = plot_label_distributions(\n", + " partitioner,\n", + " label_name=\"label\",\n", + " plot_type=\"bar\",\n", + " size_unit=\"absolute\",\n", + " partition_id_axis=\"x\",\n", + " legend=True,\n", + " verbose_labels=True,\n", + " max_num_partitions=30, # Note we are only showing the first 30 so the plot remains readable\n", + " title=\"Per Partition Labels Distribution\",\n", + ")" ] }, { @@ -583,30 +549,17 @@ "\n", "Let's next define how our FL clients will behave\n", "\n", - "## Defining a Flower Client\n", + "## Defining a Flower `ClientApp`\n", "\n", - "You can think of a client in FL as an entity that owns some data and trains a model using this data. The caveat is that the model is being trained _collaboratively_ in Federation by multiple clients (sometimes up to hundreds of thousands) and, in most instances of FL, is sent by a central server.\n", + "You can think of a client in FL as an entity that owns some data and trains a model using this data. The caveat is that the model is being trained _collaboratively_ in Federation by multiple clients (sometimes up to hundreds of thousands) and, in most instances of FL, is sent by a central server running in a `ServerApp` (more on this later).\n", "\n", - "A Flower Client is a simple Python class with four distinct methods:\n", + "A Flower Client is a simple Python class with two distinct methods:\n", "\n", "* `fit()`: With this method, the client does on-device training for a number of epochs using its own data. At the end, the resulting model is sent back to the server for aggregation.\n", "\n", "* `evaluate()`: With this method, the server can evaluate the performance of the global model on the local validation set of a client. This can be used for instance when there is no centralised dataset on the server for validation/test. Also, this method can be use to asses the degree of personalisation of the model being federated.\n", "\n", - "* `set_parameters()`: This method takes the parameters sent by the server and uses them to initialise the parameters of the local model that is ML framework specific (e.g. TF, Pytorch, etc).\n", - "\n", - "* `get_parameters()`: It extract the parameters from the local model and transforms them into a list of NumPy arrays. This ML framework-agnostic representation of the model will be sent to the server.\n", - "\n", - "Let's start by importing Flower!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import flwr as fl" + "This class will be then wrapped into a `ClientApp` that can be used to launch the simulation." ] }, { @@ -628,9 +581,10 @@ "\n", "import torch\n", "from flwr.common import NDArrays, Scalar\n", + "from flwr.client import NumPyClient\n", "\n", "\n", - "class FlowerClient(fl.client.NumPyClient):\n", + "class FlowerClient(NumPyClient):\n", " def __init__(self, trainloader, valloader) -> None:\n", " super().__init__()\n", "\n", @@ -638,74 +592,156 @@ " self.valloader = valloader\n", " self.model = Net(num_classes=10)\n", "\n", - " def set_parameters(self, parameters):\n", - " \"\"\"With the model parameters received from the server,\n", - " overwrite the uninitialise model in this class with them.\"\"\"\n", - "\n", - " params_dict = zip(self.model.state_dict().keys(), parameters)\n", - " state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict})\n", - " # now replace the parameters\n", - " self.model.load_state_dict(state_dict, strict=True)\n", - "\n", - " def get_parameters(self, config: Dict[str, Scalar]):\n", - " \"\"\"Extract all model parameters and convert them to a list of\n", - " NumPy arrays. The server doesn't work with PyTorch/TF/etc.\"\"\"\n", - " return [val.cpu().numpy() for _, val in self.model.state_dict().items()]\n", - "\n", " def fit(self, parameters, config):\n", - " \"\"\"This method train the model using the parameters sent by the\n", + " \"\"\"This method trains the model using the parameters sent by the\n", " server on the dataset of this client. At then end, the parameters\n", " of the locally trained model are communicated back to the server\"\"\"\n", "\n", " # copy parameters sent by the server into client's local model\n", - " self.set_parameters(parameters)\n", + " set_params(self.model, parameters)\n", "\n", - " # Define the optimizer -------------------------------------------------------------- Essentially the same as in the centralised example above\n", + " # Define the optimizer\n", " optim = torch.optim.SGD(self.model.parameters(), lr=0.01, momentum=0.9)\n", "\n", - " # do local training -------------------------------------------------------------- Essentially the same as in the centralised example above (but now using the client's data instead of the whole dataset)\n", + " # do local training (call same function as centralised setting)\n", " train(self.model, self.trainloader, optim, epochs=1)\n", "\n", " # return the model parameters to the server as well as extra info (number of training examples in this case)\n", - " return self.get_parameters({}), len(self.trainloader), {}\n", + " return get_params(self.model), len(self.trainloader), {}\n", "\n", " def evaluate(self, parameters: NDArrays, config: Dict[str, Scalar]):\n", " \"\"\"Evaluate the model sent by the server on this client's\n", " local validation set. Then return performance metrics.\"\"\"\n", "\n", - " self.set_parameters(parameters)\n", - " loss, accuracy = test(\n", - " self.model, self.valloader\n", - " ) # <-------------------------- calls the `test` function, just what we did in the centralised setting (but this time using the client's local validation set)\n", + " set_params(self.model, parameters)\n", + " # do local evaluation (call same function as centralised setting)\n", + " loss, accuracy = test(self.model, self.valloader)\n", " # send statistics back to the server\n", - " return float(loss), len(self.valloader), {\"accuracy\": accuracy}" + " return float(loss), len(self.valloader), {\"accuracy\": accuracy}\n", + "\n", + "\n", + "# Two auxhiliary functions to set and extract parameters of a model\n", + "def set_params(model, parameters):\n", + " \"\"\"Replace model parameters with those passed as `parameters`.\"\"\"\n", + "\n", + " params_dict = zip(model.state_dict().keys(), parameters)\n", + " state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict})\n", + " # now replace the parameters\n", + " model.load_state_dict(state_dict, strict=True)\n", + "\n", + "\n", + "def get_params(model):\n", + " \"\"\"Extract model parameters as a list of NumPy arrays.\"\"\"\n", + " return [val.cpu().numpy() for _, val in model.state_dict().items()]" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Spend a few minutes to inspect the `FlowerClient` class above. Please ask questions if there is something unclear !\n", "\n", - "Then keen-eyed among you might have realised that if we were to fuse the client's `fit()` and `evaluate()` methods, we'll end up with essentially the same as in the `run_centralised()` function we used in the Centralised Training part of this tutorial. And it is true!! In Federated Learning, the way clients perform local training makes use of the same principles as more traditional centralised setup. The key difference is that the dataset now is much smaller and it's never _\"seen\"_ by the entity running the FL workload (i.e. the central server).\n", + "Then keen-eyed among you might have realised that if we were to fuse the client's `fit()` and `evaluate()` methods, we'll end up with essentially the same as in the `run_centralised()` function we used in the Centralised Training part of this tutorial. And it is true!! In Federated Learning, the way clients perform local training makes use of the same principles as more traditional centralised setup. The key difference is that the dataset now is much smaller and it's never _\"seen\"_ by the entity running the FL workload (i.e. the central server).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `client_fn` callback\n", "\n", + "Now let's see how the `FlowerClient` object above can be used in Flower: we need to construct a `ClientApp`. This can be conveniently be done by means of a `client_fn` callback that will return a `FlowerClient` that uses a specific data partition (`partition-id`). The index of the partition is set internally during the simulation (meaning you shouldn't worry about it this tutorial)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from flwr.common import Context\n", + "from flwr.client import ClientApp\n", + "\n", + "\n", + "def client_fn(context: Context):\n", + " \"\"\"Returns a FlowerClient containing its data partition.\"\"\"\n", + "\n", + " partition_id = int(context.node_config[\"partition-id\"])\n", + " partition = fds.load_partition(partition_id, \"train\")\n", + " # partition into train/validation\n", + " partition_train_val = partition.train_test_split(test_size=0.1, seed=42)\n", + "\n", + " # Let's use the function defined earlier to construct the dataloaders\n", + " # and apply the dataset transformations\n", + " trainloader, testloader = get_mnist_dataloaders(partition_train_val, batch_size=32)\n", + "\n", + " return FlowerClient(trainloader=trainloader, valloader=testloader).to_client()\n", "\n", - "Talking about the central server... we should define what strategy we want to make use of so the updated models sent from the clients back to the server at the end of the `fit()` method are aggregate.\n", "\n", + "# Concstruct the ClientApp passing the client generation function\n", + "client_app = ClientApp(client_fn=client_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that a `ClientApp` is fully defined, let's create its counterpart: the `ServerApp`." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Defining a Flower `ServerApp`\n", "\n", - "## Chosing a Flower Strategy\n", + "### Chosing a Flower Strategy\n", "\n", "\n", "A strategy sits at the core of the Federated Learning experiment. It is involved in all stages of a FL pipeline: sampling clients; sending the _global model_ to the clients so they can do `fit()`; receive the updated models from the clients and **aggregate** these to construct a new _global model_; define and execute global or federated evaluation; and more.\n", "\n", - "Flower comes with [many strategies built-in](https://github.com/adap/flower/tree/main/src/py/flwr/server/strategy) and more to be available in the next release (`1.5` already!). For this tutorial, let's use what is arguable the most popular strategy out there: `FedAvg`.\n", + "Flower comes with [many strategies built-in](https://github.com/adap/flower/tree/main/src/py/flwr/server/strategy). For this tutorial, let's use what is arguable the most popular strategy out there: `FedAvg`.\n", "\n", "The way `FedAvg` works is simple but performs surprisingly well in practice. It is therefore one good strategy to start your experimentation. `FedAvg`, as its name implies, derives a new version of the _global model_ by taking the average of all the models sent by clients participating in the round. You can read all the details [in the paper](https://arxiv.org/abs/1602.05629).\n", "\n", - "Let's see how we can define `FedAvg` using Flower. We use one of the callbacks called `evaluate_fn` so we can easily evaluate the state of the global model using a small centralised testset. Note this functionality is user-defined since it requires a choice in terms of ML-framework. (if you recall, Flower is framework agnostic).\n", + "While Flower strategies offer a high degree of customization using callbacks, in this tutorial we'll focus on using just one: the `evaluate_metrics_aggregation_fn` callback. It allows you to pass a function that should be executed at the end of an _\"evaluate\"_ round (i.e. a round where clients evaluate the _global model_ they receive on their local data and report the result -- e.g. accuracy, loss, etc -- back to the server). For this tutorial we want to perform the weighted average of the _\"accuracy\"_ metrics returned by each `FlowerClient`'s `evaluate()` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List\n", + "from flwr.common import Metrics\n", + "\n", + "\n", + "# Define metric aggregation function\n", + "def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics:\n", + " # Multiply accuracy of each client by number of examples used\n", + " accuracies = [num_examples * m[\"accuracy\"] for num_examples, m in metrics]\n", + " examples = [num_examples for num_examples, _ in metrics]\n", "\n", - "> This being said, centralised evaluation of the global model is only possible if there exists a centralised dataset that somewhat follows a similar distribution as the data that's spread across clients. In some cases having such centralised dataset for validation is not possible, so the only solution is to federate the evaluation of the _global model_. This is the default behaviour in Flower. If you don't specify teh `evaluate_fn` argument in your strategy, then, centralised global evaluation won't be performed." + " # Aggregate and return custom metric (weighted average)\n", + " return {\"accuracy\": sum(accuracies) / sum(examples)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll use this callback when defining the strategy in the next section" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `server_fn` callback\n", + "\n", + "The easiest way to create a `ServerApp` with the aggregation _strategy_ of your choice is by means of a `server_fn` callback. It has a similar signature to `client_fn` but, instead of returning a client object, it returns all the components needed to run the server-side logic in Flower. In this tutorial we'll keep things simple and stick to `FedAvg` with initialised global parameters." ] }, { @@ -714,54 +750,47 @@ "metadata": {}, "outputs": [], "source": [ - "def get_evaluate_fn(testloader):\n", - " \"\"\"This is a function that returns a function. The returned\n", - " function (i.e. `evaluate_fn`) will be executed by the strategy\n", - " at the end of each round to evaluate the stat of the global\n", - " model.\"\"\"\n", + "from flwr.common import ndarrays_to_parameters\n", + "from flwr.server import ServerApp, ServerConfig, ServerAppComponents\n", + "from flwr.server.strategy import FedAvg\n", "\n", - " def evaluate_fn(server_round: int, parameters, config):\n", - " \"\"\"This function is executed by the strategy it will instantiate\n", - " a model and replace its parameters with those from the global model.\n", - " The, the model will be evaluate on the test set (recall this is the\n", - " whole MNIST test set).\"\"\"\n", + "num_rounds = 5\n", "\n", - " model = Net(num_classes=10)\n", "\n", - " # set parameters to the model\n", - " params_dict = zip(model.state_dict().keys(), parameters)\n", - " state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict})\n", - " model.load_state_dict(state_dict, strict=True)\n", + "def server_fn(context: Context):\n", "\n", - " # call test\n", - " loss, accuracy = test(\n", - " model, testloader\n", - " ) # <-------------------------- calls the `test` function, just what we did in the centralised setting\n", - " return loss, {\"accuracy\": accuracy}\n", + " # instantiate the model\n", + " model = Net(num_classes=10)\n", + " ndarrays = get_params(model)\n", + " # Convert model parameters to flwr.common.Parameters\n", + " global_model_init = ndarrays_to_parameters(ndarrays)\n", + "\n", + " # Define the strategy\n", + " strategy = FedAvg(\n", + " fraction_fit=0.1, # 10% clients sampled each round to do fit()\n", + " fraction_evaluate=0.5, # 50% clients sample each round to do evaluate()\n", + " evaluate_metrics_aggregation_fn=weighted_average, # callback defined earlier\n", + " initial_parameters=global_model_init, # initialised global model\n", + " )\n", + "\n", + " # Construct ServerConfig\n", + " config = ServerConfig(num_rounds=num_rounds)\n", "\n", - " return evaluate_fn\n", + " # Wrap everything into a `ServerAppComponents` object\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", "\n", "\n", - "# now we can define the strategy\n", - "strategy = fl.server.strategy.FedAvg(\n", - " fraction_fit=0.1, # let's sample 10% of the client each round to do local training\n", - " fraction_evaluate=0.1, # after each round, let's sample 20% of the clients to asses how well the global model is doing\n", - " min_available_clients=100, # total number of clients available in the experiment\n", - " evaluate_fn=get_evaluate_fn(testloader),\n", - ") # a callback to a function that the strategy can execute to evaluate the state of the global model on a centralised dataset" + "# Create your ServerApp\n", + "server_app = ServerApp(server_fn=server_fn)" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "So far we have:\n", - "* created the dataset partitions (one for each client)\n", - "* defined the client class\n", - "* decided on a strategy to use\n", + "## Launching the Simulation\n", "\n", - "Now we just need to launch the Flower FL experiment... not so fast! just one final function: let's create another callback that the Simulation Engine will use in order to span VirtualClients. As you can see this is really simple: construct a FlowerClient object, assigning each their own data partition." + "With both `ClientApp` and `ServerApp` ready, we can launch the simulation. Pass both apps to the `run_simulation()` function and specify the number of `supernodes` (this is a more general term used in Flower to refer to individual \"nodes\" or \"clients\"). We earlier partitioned the dataset into 100 partitions, one for each supernode. So we indicate that `num_supernodes`=100." ] }, { @@ -770,18 +799,97 @@ "metadata": {}, "outputs": [], "source": [ - "def generate_client_fn(trainloaders, valloaders):\n", - " def client_fn(cid: str):\n", - " \"\"\"Returns a FlowerClient containing the cid-th data partition\"\"\"\n", + "from flwr.simulation import run_simulation\n", + "\n", + "run_simulation(\n", + " server_app=server_app, client_app=client_app, num_supernodes=NUM_PARTITIONS\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note above how the distributed `accuracy` goes up as training progresses while the loss goes down. Federated learning is working!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bonus: Create your own Strategy\n", + "\n", + "Flower strategies can be extended easily to suit your FL setups or your preferred workflows whether you use Flower for research or in production. In this final section, you'll learn how to create a custom strategy that behaves just like `FedAvg` but extends the functionality of certain methods to achieve two things:\n", + "1. Save the results obtained on each round into a JSON file.\n", + "2. Create a plot at after the last round.\n", "\n", - " return FlowerClient(\n", - " trainloader=trainloaders[int(cid)], valloader=valloaders[int(cid)]\n", - " ).to_client()\n", "\n", - " return client_fn\n", + "Let's call this strategy `FedAvgCustom`. We'll use it to also showcase how to use the `evaluate_fn` callback, a convenient way to do centralised evaluation of the global model after each round. Note this functionality is user-defined since it requires a choice in terms of ML-framework. (if you recall, Flower is framework agnostic).\n", + "\n", + "> This being said, centralised evaluation of the global model is only possible if there exists a centralised dataset that somewhat follows a similar distribution as the data that's spread across clients. In some cases having such centralised dataset for validation is not possible, so the only solution is to federate the evaluation of the _global model_. This is the default behaviour in Flower. If you don't specify the `evaluate_fn` argument in your strategy, then, centralised global evaluation won't be performed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from flwr.server.strategy import FedAvg\n", + "from flwr.common import Parameters\n", + "import json\n", + "\n", + "\n", + "class FedAvgCustom(FedAvg):\n", + " def __init__(self, file_name: str, num_rounds: int, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + " self.file_name = file_name\n", + " self.num_rounds = num_rounds\n", + " self.loss_list = []\n", + " self.metrics_list = []\n", + "\n", + " def _make_plot(self):\n", + " \"\"\"Makes a plot with the results recorded\"\"\"\n", + " round = list(range(1, len(self.loss_list) + 1))\n", + " acc = [100.0 * metrics[\"accuracy\"] for metrics in self.metrics_list]\n", + " plt.plot(round, acc)\n", + " plt.grid()\n", + " plt.ylabel(\"Accuracy (%)\")\n", + " plt.xlabel(\"Round\")\n", + "\n", + " def evaluate(self, server_round: int, parameters: Parameters):\n", + " \"\"\"Evaluate model parameters using an evaluation function.\"\"\"\n", + " loss, metrics = super().evaluate(server_round, parameters)\n", + " # Record results\n", + " self.loss_list.append(loss)\n", + " self.metrics_list.append(metrics)\n", + " # If last round, save results and make a plot\n", + " if server_round == self.num_rounds:\n", + " # Save to CSV\n", + " with open(f\"{self.file_name}.json\", \"w\") as f:\n", + " json.dump({\"loss\": self.loss_list, \"metrics\": self.metrics_list}, f)\n", + " # Generate plot\n", + " self._make_plot()\n", + "\n", + "\n", + "def get_evaluate_fn(testloader):\n", + " \"\"\"Return a function that can be called to do global evaluation.\"\"\"\n", + "\n", + " def evaluate_fn(server_round: int, parameters, config):\n", + " \"\"\"Evaluate global model on the whole test set.\"\"\"\n", + "\n", + " model = Net(num_classes=10)\n", + "\n", + " # set parameters to the model\n", + " params_dict = zip(model.state_dict().keys(), parameters)\n", + " state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict})\n", + " model.load_state_dict(state_dict, strict=True)\n", "\n", + " # call test (evaluate model as in centralised setting)\n", + " loss, accuracy = test(model, testloader)\n", + " return loss, {\"accuracy\": accuracy}\n", "\n", - "client_fn_callback = generate_client_fn(trainloaders, valloaders)" + " return evaluate_fn" ] }, { @@ -789,7 +897,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we are ready to launch the FL experiment using Flower simulation:" + "With the above defined, we just need to wrap it all up in a `ServerApp` as we did earlier but this time using the `FedAvgCustom` that we just defined." ] }, { @@ -803,46 +911,57 @@ }, "outputs": [], "source": [ - "history = fl.simulation.start_simulation(\n", - " client_fn=client_fn_callback, # a callback to construct a client\n", - " num_clients=100, # total number of clients in the experiment\n", - " config=fl.server.ServerConfig(num_rounds=10), # let's run for 10 rounds\n", - " strategy=strategy, # the strategy that will orchestrate the whole FL pipeline\n", - ")" + "from flwr.server import ServerApp, ServerConfig\n", + "\n", + "\n", + "def server_fn(context: Context):\n", + "\n", + " # instantiate the model\n", + " model = Net(num_classes=10)\n", + " ndarrays = get_params(model)\n", + " # Convert model parameters to flwr.common.Parameters\n", + " global_model_init = ndarrays_to_parameters(ndarrays)\n", + "\n", + " # Define the strategy\n", + " strategy = FedAvgCustom(\n", + " file_name=\"results_fedavgcustom\",\n", + " num_rounds=num_rounds,\n", + " fraction_fit=0.1, # 10% clients sampled each round to do fit()\n", + " fraction_evaluate=0.25, # 25% clients sample each round to do evaluate()\n", + " evaluate_metrics_aggregation_fn=weighted_average, # callback defined earlier\n", + " initial_parameters=global_model_init, # initialised global model\n", + " evaluate_fn=get_evaluate_fn(\n", + " testloader\n", + " ), # gloabl evaluation (here we can pass the same testset as used in centralised)\n", + " )\n", + "\n", + " # Construct ServerConfig\n", + " config = ServerConfig(num_rounds=num_rounds)\n", + "\n", + " # Wrap everything into a `ServerAppComponents` object\n", + " return ServerAppComponents(strategy=strategy, config=config)\n", + "\n", + "\n", + "# Create your ServerApp\n", + "server_app = ServerApp(server_fn=server_fn)" ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Doing 10 rounds should take less than 2 minutes on a CPU-only Colab instance <-- Flower Simulation is fast! πŸš€\n", - "\n", - "You can then use the returned `History` object to either save the results to disk or do some visualisation (or both of course, or neither if you like chaos). Below you can see how you can plot the centralised accuracy obtained at the end of each round (including at the very beginning of the experiment) for the _global model_. This is want the function `evaluate_fn()` that we passed to the strategy reports." + "All that is left is to launch the simulation. Note a plot will be displayed at the end and a `.json` with the results will be saved to the current directory." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 508 - }, - "outputId": "d8eab106-cee9-4266-9082-0944882cdba8" - }, + "metadata": {}, "outputs": [], "source": [ - "print(f\"{history.metrics_centralized = }\")\n", - "\n", - "global_accuracy_centralised = history.metrics_centralized[\"accuracy\"]\n", - "round = [data[0] for data in global_accuracy_centralised]\n", - "acc = [100.0 * data[1] for data in global_accuracy_centralised]\n", - "plt.plot(round, acc)\n", - "plt.grid()\n", - "plt.ylabel(\"Accuracy (%)\")\n", - "plt.xlabel(\"Round\")\n", - "plt.title(\"MNIST - IID - 100 clients with 10 clients per round\")" + "run_simulation(\n", + " server_app=server_app, client_app=client_app, num_supernodes=NUM_PARTITIONS\n", + ")" ] }, { From e52fe37beddcca7c658c5524de70d012c35955db Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:41:40 +0200 Subject: [PATCH 034/188] refactor(examples) Refactor quickstart-sklearn-tabular to use the new Flower (#3893) Co-authored-by: jafermarq --- examples/quickstart-sklearn-tabular/README.md | 74 +++++++----------- examples/quickstart-sklearn-tabular/client.py | 73 ----------------- .../quickstart-sklearn-tabular/pyproject.toml | 45 +++++++---- .../requirements.txt | 3 - examples/quickstart-sklearn-tabular/run.sh | 17 ---- examples/quickstart-sklearn-tabular/server.py | 19 ----- .../sklearnexample/__init__.py | 1 + .../sklearnexample/client_app.py | 67 ++++++++++++++++ .../sklearnexample/server_app.py | 73 +++++++++++++++++ .../sklearnexample/task.py | 78 +++++++++++++++++++ examples/quickstart-sklearn-tabular/utils.py | 75 ------------------ 11 files changed, 278 insertions(+), 247 deletions(-) delete mode 100644 examples/quickstart-sklearn-tabular/client.py delete mode 100644 examples/quickstart-sklearn-tabular/requirements.txt delete mode 100755 examples/quickstart-sklearn-tabular/run.sh delete mode 100644 examples/quickstart-sklearn-tabular/server.py create mode 100644 examples/quickstart-sklearn-tabular/sklearnexample/__init__.py create mode 100644 examples/quickstart-sklearn-tabular/sklearnexample/client_app.py create mode 100644 examples/quickstart-sklearn-tabular/sklearnexample/server_app.py create mode 100644 examples/quickstart-sklearn-tabular/sklearnexample/task.py delete mode 100644 examples/quickstart-sklearn-tabular/utils.py diff --git a/examples/quickstart-sklearn-tabular/README.md b/examples/quickstart-sklearn-tabular/README.md index b0b4cd1b84c0..db8a6bfbfb7c 100644 --- a/examples/quickstart-sklearn-tabular/README.md +++ b/examples/quickstart-sklearn-tabular/README.md @@ -4,7 +4,7 @@ dataset: [Iris] framework: [scikit-learn] --- -# Flower Example using scikit-learn +# Federated Learning with scikit-learn and Flower (Quickstart Example) This example of Flower uses `scikit-learn`'s `LogisticRegression` model to train a federated learning system on "iris" (tabular) dataset. @@ -12,7 +12,9 @@ It will help you understand how to adapt Flower for use with `scikit-learn`. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the dataset. -## Project Setup +## Set up the project + +### Clone the project Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: @@ -20,64 +22,44 @@ Start by cloning the example project. We prepared a single-line command that you git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/quickstart-sklearn-tabular . && rm -rf flower && cd quickstart-sklearn-tabular ``` -This will create a new directory called `quickstart-sklearn-tabular` containing the following files: +This will create a new directory called `quickstart-sklearn-tabular` with the following structure: ```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- utils.py --- README.md +quickstart-sklearn-tabular +β”œβ”€β”€ sklearnexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies - -Project dependencies (such as `scikit-learn` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +### Install dependencies and project -#### Poetry +Install the dependencies defined in `pyproject.toml` as well as the `sklearnexample` package. -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +## Run the project -```shell -poetry run python3 -c "import flwr" -``` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -If you don't see any errors you're good to go! +### Run with the Simulation Engine -#### pip - -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt +```bash +flwr run . ``` -## Run Federated Learning with scikit-learn and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -poetry run python3 server.py -``` - -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminals and run the following command in each: - -```shell -poetry run python3 client.py --partition-id 0 # partition-id should be any of {0,1,2} +```bash +flwr run . --run-config penalty="'l1'" ``` -Alternatively you can run all of it in one shell as follows: - -```shell -poetry run python3 server.py & -poetry run python3 client.py --partition-id 0 & -poetry run python3 client.py --partition-id 1 -``` +### Run with the Deployment Engine -You will see that Flower is starting a federated training. +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-sklearn-tabular/client.py b/examples/quickstart-sklearn-tabular/client.py deleted file mode 100644 index b7e3046c822d..000000000000 --- a/examples/quickstart-sklearn-tabular/client.py +++ /dev/null @@ -1,73 +0,0 @@ -import argparse -import warnings - -from sklearn.linear_model import LogisticRegression -from sklearn.metrics import log_loss - -import flwr as fl -import utils -from flwr_datasets import FederatedDataset - -if __name__ == "__main__": - N_CLIENTS = 3 - - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--partition-id", - type=int, - choices=range(0, N_CLIENTS), - required=True, - help="Specifies the artificial data partition", - ) - args = parser.parse_args() - partition_id = args.partition_id - - # Load the partition data - fds = FederatedDataset(dataset="hitorilabs/iris", partitioners={"train": N_CLIENTS}) - - dataset = fds.load_partition(partition_id, "train").with_format("pandas")[:] - X = dataset[["petal_length", "petal_width", "sepal_length", "sepal_width"]] - y = dataset["species"] - unique_labels = fds.load_split("train").unique("species") - # Split the on edge data: 80% train, 20% test - X_train, X_test = X[: int(0.8 * len(X))], X[int(0.8 * len(X)) :] - y_train, y_test = y[: int(0.8 * len(y))], y[int(0.8 * len(y)) :] - - # Create LogisticRegression Model - model = LogisticRegression( - penalty="l2", - max_iter=1, # local epoch - warm_start=True, # prevent refreshing weights when fitting - ) - - # Setting initial parameters, akin to model.compile for keras models - utils.set_initial_params(model, n_features=X_train.shape[1], n_classes=3) - - # Define Flower client - class IrisClient(fl.client.NumPyClient): - def get_parameters(self, config): # type: ignore - return utils.get_model_parameters(model) - - def fit(self, parameters, config): # type: ignore - utils.set_model_params(model, parameters) - # Ignore convergence failure due to low local epochs - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - model.fit(X_train, y_train) - accuracy = model.score(X_train, y_train) - return ( - utils.get_model_parameters(model), - len(X_train), - {"train_accuracy": accuracy}, - ) - - def evaluate(self, parameters, config): # type: ignore - utils.set_model_params(model, parameters) - loss = log_loss(y_test, model.predict_proba(X_test), labels=unique_labels) - accuracy = model.score(X_test, y_test) - return loss, len(X_test), {"test_accuracy": accuracy} - - # Start Flower client - fl.client.start_client( - server_address="0.0.0.0:8080", client=IrisClient().to_client() - ) diff --git a/examples/quickstart-sklearn-tabular/pyproject.toml b/examples/quickstart-sklearn-tabular/pyproject.toml index 86eab5c38df0..2f2775e9fe90 100644 --- a/examples/quickstart-sklearn-tabular/pyproject.toml +++ b/examples/quickstart-sklearn-tabular/pyproject.toml @@ -1,18 +1,35 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "sklearn-mnist" -version = "0.1.0" -description = "Federated learning with scikit-learn and Flower" -authors = [ - "The Flower Authors ", - "Kaushik Amar Das ", +[project] +name = "sklearnexample" +version = "1.0.0" +description = "Federated Learning with scikit-learn and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "scikit-learn>=1.3.0", ] -[tool.poetry.dependencies] -python = "^3.8" -flwr = ">=1.0,<2.0" -flwr-datasets = { extras = ["vision"], version = ">=0.0.2,<1.0.0" } -scikit-learn = "^1.3.0" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "sklearnexample.server_app:app" +clientapp = "sklearnexample.client_app:app" + +[tool.flwr.app.config] +penalty = "l2" +num-server-rounds = 25 +min-available-clients = 2 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 3 diff --git a/examples/quickstart-sklearn-tabular/requirements.txt b/examples/quickstart-sklearn-tabular/requirements.txt deleted file mode 100644 index e0f15b31f3f7..000000000000 --- a/examples/quickstart-sklearn-tabular/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flwr>=1.0, <2.0 -flwr-datasets[vision]>=0.0.2, <1.0.0 -scikit-learn>=1.3.0 diff --git a/examples/quickstart-sklearn-tabular/run.sh b/examples/quickstart-sklearn-tabular/run.sh deleted file mode 100755 index f770ca05f8f4..000000000000 --- a/examples/quickstart-sklearn-tabular/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in $(seq 0 1); do - echo "Starting client $i" - python client.py --partition-id "${i}" & -done - -# This will allow you to use CTRL+C to stop all background processes -trap 'trap - SIGTERM && kill -- -$$' SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-sklearn-tabular/server.py b/examples/quickstart-sklearn-tabular/server.py deleted file mode 100644 index 0c779c52a8d6..000000000000 --- a/examples/quickstart-sklearn-tabular/server.py +++ /dev/null @@ -1,19 +0,0 @@ -import flwr as fl -import utils -from sklearn.linear_model import LogisticRegression - - -# Start Flower server for five rounds of federated learning -if __name__ == "__main__": - model = LogisticRegression() - utils.set_initial_params(model, n_classes=3, n_features=4) - strategy = fl.server.strategy.FedAvg( - min_available_clients=2, - fit_metrics_aggregation_fn=utils.weighted_average, - evaluate_metrics_aggregation_fn=utils.weighted_average, - ) - fl.server.start_server( - server_address="0.0.0.0:8080", - strategy=strategy, - config=fl.server.ServerConfig(num_rounds=25), - ) diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/__init__.py b/examples/quickstart-sklearn-tabular/sklearnexample/__init__.py new file mode 100644 index 000000000000..ddcfc4fc7fc4 --- /dev/null +++ b/examples/quickstart-sklearn-tabular/sklearnexample/__init__.py @@ -0,0 +1 @@ +"""quickstart-sklearn-example.""" diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py b/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py new file mode 100644 index 000000000000..fb1e581c978a --- /dev/null +++ b/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py @@ -0,0 +1,67 @@ +"""sklearnexample: A Flower / sklearn app.""" + +import warnings + +from sklearn.metrics import log_loss +from flwr.common import Context +from flwr.client import NumPyClient, ClientApp + +from sklearnexample.task import ( + create_log_reg_and_instantiate_parameters, + set_model_params, + get_model_parameters, + load_data, + UNIQUE_LABELS, +) + + +class FlowerClient(NumPyClient): + def __init__(self, model, X_train, y_train, X_test, y_test): + self.model = model + self.X_train = X_train.values + self.y_train = y_train.values + self.X_test = X_test.values + self.y_test = y_test.values + self.unique_labels = UNIQUE_LABELS + + def fit(self, parameters, config): + set_model_params(self.model, parameters) + # Ignore convergence failure due to low local epochs + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.model.fit(self.X_train, self.y_train) + accuracy = self.model.score(self.X_train, self.y_train) + return ( + get_model_parameters(self.model), + len(self.X_train), + {"train_accuracy": accuracy}, + ) + + def evaluate(self, parameters, config): # type: ignore + set_model_params(self.model, parameters) + y_pred = self.model.predict_proba(self.X_test) + loss = log_loss(self.y_test, y_pred, labels=self.unique_labels) + accuracy = self.model.score(self.X_test, self.y_test) + return loss, len(self.X_test), {"test_accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + X_train, y_train, X_test, y_test = load_data(partition_id, num_partitions) + + # Read the run config to get settings to configure the Client + penalty = context.run_config["penalty"] + + # Create LogisticRegression Model + model = create_log_reg_and_instantiate_parameters(penalty) + + # Return Client instance + return FlowerClient(model, X_train, y_train, X_test, y_test).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py b/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py new file mode 100644 index 000000000000..a99a74a140c0 --- /dev/null +++ b/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py @@ -0,0 +1,73 @@ +"""sklearnexample: A Flower / sklearn app.""" + +from typing import List, Tuple, Dict + +from flwr.common import Metrics, Scalar, Context, ndarrays_to_parameters +from flwr.server import ServerAppComponents, ServerConfig, ServerApp +from flwr.server.strategy import FedAvg + +from sklearnexample.task import ( + create_log_reg_and_instantiate_parameters, + get_model_parameters, +) + + +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Dict[str, Scalar]: + """Compute weighted average. + + It is a generic implementation that averages only over floats and ints and drops the + other data types of the Metrics. + """ + # num_samples_list can represent the number of samples + # or the number of batches depending on the client + num_samples_list = [n_batches for n_batches, _ in metrics] + num_samples_sum = sum(num_samples_list) + metrics_lists: Dict[str, List[float]] = {} + for num_samples, all_metrics_dict in metrics: + # Calculate each metric one by one + for single_metric, value in all_metrics_dict.items(): + if isinstance(value, (float, int)): + metrics_lists[single_metric] = [] + # Just one iteration needed to initialize the keywords + break + + for num_samples, all_metrics_dict in metrics: + # Calculate each metric one by one + for single_metric, value in all_metrics_dict.items(): + # Add weighted metric + if isinstance(value, (float, int)): + metrics_lists[single_metric].append(float(num_samples * value)) + + weighted_metrics: Dict[str, Scalar] = {} + for metric_name, metric_values in metrics_lists.items(): + weighted_metrics[metric_name] = sum(metric_values) / num_samples_sum + + return weighted_metrics + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components that set the ServerApp behavior.""" + + penalty = context.run_config["penalty"] + model = create_log_reg_and_instantiate_parameters(penalty) + ndarrays = get_model_parameters(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define the strategy + min_available_clients = context.run_config["min-available-clients"] + strategy = FedAvg( + min_available_clients=min_available_clients, + fit_metrics_aggregation_fn=weighted_average, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=global_model_init, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/task.py b/examples/quickstart-sklearn-tabular/sklearnexample/task.py new file mode 100644 index 000000000000..db4d70761edd --- /dev/null +++ b/examples/quickstart-sklearn-tabular/sklearnexample/task.py @@ -0,0 +1,78 @@ +import numpy as np +from sklearn.linear_model import LogisticRegression + +from flwr.common import NDArrays +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + +# This information is needed to create a correct scikit-learn model +UNIQUE_LABELS = [0, 1, 2] +FEATURES = ["petal_length", "petal_width", "sepal_length", "sepal_width"] + + +def get_model_parameters(model: LogisticRegression) -> NDArrays: + """Return the parameters of a sklearn LogisticRegression model.""" + if model.fit_intercept: + params = [ + model.coef_, + model.intercept_, + ] + else: + params = [ + model.coef_, + ] + return params + + +def set_model_params(model: LogisticRegression, params: NDArrays) -> LogisticRegression: + """Set the parameters of a sklean LogisticRegression model.""" + model.coef_ = params[0] + if model.fit_intercept: + model.intercept_ = params[1] + return model + + +def set_initial_params(model: LogisticRegression, n_classes: int, n_features: int): + """Set initial parameters as zeros. + + Required since model params are uninitialized until model.fit is called but server + asks for initial parameters from clients at launch. Refer to + sklearn.linear_model.LogisticRegression documentation for more information. + """ + model.classes_ = np.array([i for i in range(n_classes)]) + + model.coef_ = np.zeros((n_classes, n_features)) + if model.fit_intercept: + model.intercept_ = np.zeros((n_classes,)) + + +def create_log_reg_and_instantiate_parameters(penalty): + model = LogisticRegression( + penalty=penalty, + max_iter=1, # local epoch + warm_start=True, # prevent refreshing weights when fitting, + solver="saga", + ) + # Setting initial parameters, akin to model.compile for keras models + set_initial_params(model, n_features=len(FEATURES), n_classes=len(UNIQUE_LABELS)) + return model + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + """Load the data for the given partition.""" + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="hitorilabs/iris", partitioners={"train": partitioner} + ) + dataset = fds.load_partition(partition_id, "train").with_format("pandas")[:] + X = dataset[FEATURES] + y = dataset["species"] + # Split the on-edge data: 80% train, 20% test + X_train, X_test = X[: int(0.8 * len(X))], X[int(0.8 * len(X)) :] + y_train, y_test = y[: int(0.8 * len(y))], y[int(0.8 * len(y)) :] + return X_train, y_train, X_test, y_test diff --git a/examples/quickstart-sklearn-tabular/utils.py b/examples/quickstart-sklearn-tabular/utils.py deleted file mode 100644 index e154f44ef8bf..000000000000 --- a/examples/quickstart-sklearn-tabular/utils.py +++ /dev/null @@ -1,75 +0,0 @@ -from typing import List, Tuple, Dict - -import numpy as np -from sklearn.linear_model import LogisticRegression - -from flwr.common import NDArrays, Metrics, Scalar - - -def get_model_parameters(model: LogisticRegression) -> NDArrays: - """Return the parameters of a sklearn LogisticRegression model.""" - if model.fit_intercept: - params = [ - model.coef_, - model.intercept_, - ] - else: - params = [ - model.coef_, - ] - return params - - -def set_model_params(model: LogisticRegression, params: NDArrays) -> LogisticRegression: - """Set the parameters of a sklean LogisticRegression model.""" - model.coef_ = params[0] - if model.fit_intercept: - model.intercept_ = params[1] - return model - - -def set_initial_params(model: LogisticRegression, n_classes: int, n_features: int): - """Set initial parameters as zeros. - - Required since model params are uninitialized until model.fit is called but server - asks for initial parameters from clients at launch. Refer to - sklearn.linear_model.LogisticRegression documentation for more information. - """ - model.classes_ = np.array([i for i in range(n_classes)]) - - model.coef_ = np.zeros((n_classes, n_features)) - if model.fit_intercept: - model.intercept_ = np.zeros((n_classes,)) - - -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Dict[str, Scalar]: - """Compute weighted average. - - It is generic implementation that averages only over floats and ints and drops the - other data types of the Metrics. - """ - print(metrics) - # num_samples_list can represent number of sample or batches depending on the client - num_samples_list = [n_batches for n_batches, _ in metrics] - num_samples_sum = sum(num_samples_list) - metrics_lists: Dict[str, List[float]] = {} - for num_samples, all_metrics_dict in metrics: - # Calculate each metric one by one - for single_metric, value in all_metrics_dict.items(): - if isinstance(value, (float, int)): - metrics_lists[single_metric] = [] - # Just one iteration needed to initialize the keywords - break - - for num_samples, all_metrics_dict in metrics: - # Calculate each metric one by one - for single_metric, value in all_metrics_dict.items(): - # Add weighted metric - if isinstance(value, (float, int)): - metrics_lists[single_metric].append(float(num_samples * value)) - - weighted_metrics: Dict[str, Scalar] = {} - for metric_name, metric_values in metrics_lists.items(): - weighted_metrics[metric_name] = sum(metric_values) / num_samples_sum - - return weighted_metrics From 581108b875044cedaa9dd1104d6d34a02b94a9b9 Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 9 Aug 2024 01:30:56 +0100 Subject: [PATCH 035/188] docs(examples) Update quickstart tutorial for PyTorch (#3925) --- doc/source/tutorial-quickstart-mlx.rst | 52 ++- doc/source/tutorial-quickstart-pytorch.rst | 428 ++++++++++++--------- 2 files changed, 271 insertions(+), 209 deletions(-) diff --git a/doc/source/tutorial-quickstart-mlx.rst b/doc/source/tutorial-quickstart-mlx.rst index 593603cd8ae2..6aa7c8d3aa13 100644 --- a/doc/source/tutorial-quickstart-mlx.rst +++ b/doc/source/tutorial-quickstart-mlx.rst @@ -5,21 +5,28 @@ Quickstart MLX ============== -In this tutorial we will learn how to train simple MLP on MNIST using Flower and MLX. - -First of all, it is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. +In this federated learning tutorial we will learn how to train simple MLP on MNIST using Flower and MLX. It is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. Let's use `flwr new` to create a complete Flower+MLX project. It will generate all the files needed to run, by default with the Simulation Engine, a federation of 10 nodes using `FedAvg `_. The dataset will be partitioned using Flower Dataset's `IidPartitioner `_. -Now that we have a rough idea of what this example is about, let's get started. We first need to create an MLX project. You can do this by running the command below. You will be prompted to give a name to your project as well as typing your developer name.: +Now that we have a rough idea of what this example is about, let's get started. First, install Flower in your new environment: + +.. code-block:: shell + + # In a new Python environment + $ pip install flwr + +Then, run the command below. You will be prompted to select of the available templates (choose :code:`MLX`), give a name to your project, and type in your developer name: .. code-block:: shell - $ flwr new --framework MLX + $ flwr new + After running it you'll notice a new directory with your project name has been created. It should have the following structure: .. code-block:: shell + β”œβ”€β”€ β”‚ β”œβ”€β”€ __init__.py @@ -44,7 +51,7 @@ To run the project do: # Run with default arguments $ flwr run . -With default argumnets you will see an output like this one: +With default arguments you will see an output like this one: .. code-block:: shell @@ -87,7 +94,7 @@ With default argumnets you will see an output like this one: INFO : -You can also override the parameters defined in `[tool.flwr.app.config]` section in the `pyproject.toml` like this: +You can also override the parameters defined in :code:`[tool.flwr.app.config]` section in the :code:`pyproject.toml` like this: .. code-block:: shell @@ -95,12 +102,12 @@ You can also override the parameters defined in `[tool.flwr.app.config]` section $ flwr run . --run-config num-server-rounds=5,lr=0.05 -What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the `ClientApp` and defining the `ServerApp`. +What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the :code:`ClientApp` and defining the :code:`ServerApp`. The Data -------- -We will use `flwr_datasets` to easily download and partition the `MNIST` dataset. +We will use `Flower Datasets `_ to easily download and partition the `MNIST` dataset. In this example you'll make use of the `IidPartitioner `_ to generate `num_partitions` partitions. You can choose `other partitioners `_ available in Flower Datasets: @@ -143,7 +150,7 @@ You can choose `other partitioners `_, it's a simple MLP: .. code-block:: python @@ -188,8 +195,8 @@ The ClientApp ------------- The main changes we have to make to use `MLX` with `Flower` will be found in -the `get_params` and `set_params` functions. Indeed, MLX doesn't -provide an easy way to convert the model parameters into a list of `np.array` objects +the :code:`get_params()` and :code:`set_params()` functions. Indeed, MLX doesn't +provide an easy way to convert the model parameters into a list of :code:`np.array` objects (the format we need for the serialization of the messages to work). The way MLX stores its parameters is as follows: @@ -205,7 +212,7 @@ The way MLX stores its parameters is as follows: ] } -Therefore, to get our list of `np.array`s, we need to extract each array and +Therefore, to get our list of :code:`np.array`s, we need to extract each array and convert them into a NumPy array: .. code-block:: python @@ -215,7 +222,7 @@ convert them into a NumPy array: return [np.array(val) for layer in layers for _, val in layer.items()] -For the `set_params` function, we perform the reverse operation. We receive +For the :code:`set_params()` function, we perform the reverse operation. We receive a list of NumPy arrays and want to convert them into MLX parameters. Therefore, we iterate through pairs of parameters and assign them to the `weight` and `bias` keys of each layer dict: @@ -231,7 +238,7 @@ keys of each layer dict: model.update(new_params) -The rest of the functionality is directly inspired by the centralized case. The `fit()` +The rest of the functionality is directly inspired by the centralized case. The :code:`fit()` method in the client trains the model using the local dataset: .. code-block:: python @@ -251,7 +258,7 @@ method in the client trains the model using the local dataset: Here, after updating the parameters, we perform the training as in the centralized case, and return the new parameters. -And for the `evaluate` method of the client: +And for the :code:`evaluate()` method of the client: .. code-block:: python @@ -264,7 +271,7 @@ And for the `evaluate` method of the client: We also begin by updating the parameters with the ones sent by the server, and then we compute the loss and accuracy using the functions defined above. In the -constructor of the `FlowerClient` we instantiate the `MLP` model as well as other +constructor of the :code:`FlowerClient` we instantiate the `MLP` model as well as other components such as the optimizer. Putting everything together we have: @@ -322,7 +329,7 @@ Putting everything together we have: return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} -Finally, we can construct a `ClientApp` using the `FlowerClient` defined above by means of a `client_fn` callback: +Finally, we can construct a :code:`ClientApp` using the :code:`FlowerClient` defined above by means of a :code:`client_fn()` callback. Note that :code:`context` enables you to get access to hyperparemeters defined in :code:`pyproject.toml` to configure the run. In this tutorial we access, among other hyperparameters, the :code:`local-epochs` setting to control the number of epochs a :code:`ClientApp` will perform when running the :code:`fit()` method. .. code-block:: python @@ -350,8 +357,8 @@ Finally, we can construct a `ClientApp` using the `FlowerClient` defined above b The ServerApp ------------- -To construct a `ServerApp` we define a `server_fn()` callback with an identical signature -to that of `client_fn()` but the return type is `ServerAppComponents `_ as opposed to a `Client `_. In this example we use the `FedAvg` strategy. +To construct a :code:`ServerApp`, we define a :code:`server_fn()` callback with an identical signature +to that of :code:`client_fn()`, but the return type is `ServerAppComponents `_ as opposed to `Client `_. In this example we use the :code:`FedAvg` strategy. .. code-block:: python @@ -372,4 +379,7 @@ to that of `client_fn()` but the return type is `ServerAppComponents `_ of the extended version of this tutorial can be found in :code:`examples/quickstart-mlx`. + +.. note:: + + Check the `source code `_ of the extended version of this tutorial in :code:`examples/quickstart-mlx` in the Flower GitHub repository. diff --git a/doc/source/tutorial-quickstart-pytorch.rst b/doc/source/tutorial-quickstart-pytorch.rst index 6eb1f283c35e..d768ae7a2fd7 100644 --- a/doc/source/tutorial-quickstart-pytorch.rst +++ b/doc/source/tutorial-quickstart-pytorch.rst @@ -4,270 +4,322 @@ Quickstart PyTorch ================== -.. meta:: - :description: Check out this Federated Learning quickstart tutorial for using Flower with PyTorch to train a CNN model on MNIST. +In this federated learning tutorial we will learn how to train a Convolutional Neural Network on CIFAR-10 using Flower and PyTorch. It is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. -.. youtube:: jOmmuzMIQ4c - :width: 100% +Let's use `flwr new` to create a complete Flower+PyTorch project. It will generate all the files needed to run, by default with the Flower Simulation Engine, a federation of 10 nodes using `FedAvg `_. The dataset will be partitioned using Flower Dataset's `IidPartitioner `_. + +Now that we have a rough idea of what this example is about, let's get started. First, install Flower in your new environment: + +.. code-block:: shell -In this tutorial we will learn how to train a Convolutional Neural Network on CIFAR10 using Flower and PyTorch. + # In a new Python environment + $ pip install flwr -First of all, it is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. +Then, run the command below. You will be prompted to select one of the available templates (choose :code:`PyTorch`), give a name to your project, and type in your developer name: -Our example consists of one *server* and two *clients* all having the same model. +.. code-block:: shell -*Clients* are responsible for generating individual weight-updates for the model based on their local datasets. -These updates are then sent to the *server* which will aggregate them to produce a better model. Finally, the *server* sends this improved version of the model back to each *client*. -A complete cycle of weight updates is called a *round*. + $ flwr new -Now that we have a rough idea of what is going on, let's get started. We first need to install Flower. You can do this by running : +After running it you'll notice a new directory with your project name has been created. It should have the following structure: .. code-block:: shell - $ pip install flwr + + β”œβ”€β”€ + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp + β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp + β”‚ └── task.py # Defines your model, training and data loading + β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs + └── README.md + -Since we want to use PyTorch to solve a computer vision task, let's go ahead and install PyTorch and the **torchvision** library: +If you haven't yet installed the project and its dependencies, you can do so by: .. code-block:: shell - $ pip install torch torchvision + # From the directory where your pyproject.toml is + $ pip install -e . +To run the project, do: -Flower Client -------------- +.. code-block:: shell -Now that we have all our dependencies installed, let's run a simple distributed training with two clients and one server. Our training procedure and network architecture are based on PyTorch's `Deep Learning with PyTorch `_. + # Run with default arguments + $ flwr run . -In a file called :code:`client.py`, import Flower and PyTorch related packages: +With default arguments you will see an output like this one: -.. code-block:: python +.. code-block:: shell + + Loading project configuration... + Success + WARNING : FAB ID is not provided; the default ClientApp will be loaded. + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Evaluating initial global parameters + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + WARNING : No fit_metrics_aggregation_fn provided + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + WARNING : No evaluate_metrics_aggregation_fn provided + INFO : + INFO : [ROUND 2] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [ROUND 3] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 21.35s + INFO : History (loss, distributed): + INFO : round 1: 2.2978184528648855 + INFO : round 2: 2.173852103948593 + INFO : round 3: 2.039920600131154 + INFO : + +You can also override the parameters defined in the :code:`[tool.flwr.app.config]` section in :code:`pyproject.toml` like this: - from collections import OrderedDict +.. code-block:: shell - import torch - import torch.nn as nn - import torch.nn.functional as F - import torchvision.transforms as transforms - from torch.utils.data import DataLoader - from torchvision.datasets import CIFAR10 + # Override some arguments + $ flwr run . --run-config num-server-rounds=5,local-epochs=3 - import flwr as fl -In addition, we define the device allocation in PyTorch with: +What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the :code:`ClientApp` and defining the :code:`ServerApp`. -.. code-block:: python - DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") +The Data +-------- + +This tutorial uses `Flower Datasets `_ to easily download and partition the `CIFAR-10` dataset. +In this example you'll make use of the `IidPartitioner `_ to generate `num_partitions` partitions. +You can choose `other partitioners `_ available in Flower Datasets. Each :code:`ClientApp` will call this function to create dataloaders with the data that correspond to their data partition. -We use PyTorch to load CIFAR10, a popular colored image classification dataset for machine learning. The PyTorch :code:`DataLoader()` downloads the training and test data that are then normalized. .. code-block:: python - def load_data(): - """Load CIFAR-10 (training and test set).""" - transform = transforms.Compose( - [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - trainset = CIFAR10(".", train=True, download=True, transform=transform) - testset = CIFAR10(".", train=False, download=True, transform=transform) - trainloader = DataLoader(trainset, batch_size=32, shuffle=True) - testloader = DataLoader(testset, batch_size=32) - num_examples = {"trainset" : len(trainset), "testset" : len(testset)} - return trainloader, testloader, num_examples + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + +The Model +--------- -Define the loss and optimizer with PyTorch. The training of the dataset is done by looping over the dataset, measure the corresponding loss and optimize it. +We defined a simple Convolutional Neural Network (CNN), but feel free to replace it with a more sophisticated model if you'd like: .. code-block:: python - def train(net, trainloader, epochs): - """Train the network on the training set.""" - criterion = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) + class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +In addition to defining the model architecture, we also include two utility functions to perform both training (i.e. :code:`train()`) and evaluation (i.e. :code:`test()`) using the above model. These functions should look fairly familiar if you have some prior experience with PyTorch. Note these functions do not have anything specific to Flower. That being said, the training function will normally be called, as we'll see later, from a Flower client passing its own data. In summary, your clients can use standard training/testing functions to perform local training or evaluation: + +.. code-block:: python + + def train(net, trainloader, epochs, device): + """Train the model on the training set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss().to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9) + net.train() + running_loss = 0.0 for _ in range(epochs): - for images, labels in trainloader: - images, labels = images.to(DEVICE), labels.to(DEVICE) + for batch in trainloader: + images = batch["img"] + labels = batch["label"] optimizer.zero_grad() - loss = criterion(net(images), labels) + loss = criterion(net(images.to(device)), labels.to(device)) loss.backward() optimizer.step() + running_loss += loss.item() -Define then the validation of the machine learning network. We loop over the test set and measure the loss and accuracy of the test set. + avg_trainloss = running_loss / len(trainloader) + return avg_trainloss -.. code-block:: python - def test(net, testloader): - """Validate the network on the entire test set.""" + def test(net, testloader, device): + """Validate the model on the test set.""" + net.to(device) criterion = torch.nn.CrossEntropyLoss() - correct, total, loss = 0, 0, 0.0 + correct, loss = 0, 0.0 with torch.no_grad(): - for data in testloader: - images, labels = data[0].to(DEVICE), data[1].to(DEVICE) + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) outputs = net(images) loss += criterion(outputs, labels).item() - _, predicted = torch.max(outputs.data, 1) - total += labels.size(0) - correct += (predicted == labels).sum().item() - accuracy = correct / total + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) return loss, accuracy -After defining the training and testing of a PyTorch machine learning model, we use the functions for the Flower clients. -The Flower clients will use a simple CNN adapted from 'PyTorch: A 60 Minute Blitz': +The ClientApp +------------- -.. code-block:: python +The main changes we have to make to use `PyTorch` with `Flower` will be found in +the :code:`get_weights()` and :code:`set_weights()` functions. In :code:`get_weights()` PyTorch model parameters are extracted and represented as a list of NumPy arrays. The :code:`set_weights()` function that's the oposite: given a list of NumPy arrays it applies them to an existing PyTorch model. Doing this in fairly easy in PyTorch. + +.. note:: + The specific implementation of :code:`get_weights()` and :code:`set_weights()` depends on the type of models you use. The ones shown below work for a wide range of PyTorch models but you might need to adjust them if you have more exotic model architectures. - class Net(nn.Module): - def __init__(self) -> None: - super(Net, self).__init__() - self.conv1 = nn.Conv2d(3, 6, 5) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, 5) - self.fc1 = nn.Linear(16 * 5 * 5, 120) - self.fc2 = nn.Linear(120, 84) - self.fc3 = nn.Linear(84, 10) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = x.view(-1, 16 * 5 * 5) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - x = self.fc3(x) - return x - - # Load model and data - net = Net().to(DEVICE) - trainloader, testloader, num_examples = load_data() - -After loading the data set with :code:`load_data()` we define the Flower interface. - -The Flower server interacts with clients through an interface called -:code:`Client`. When the server selects a particular client for training, it -sends training instructions over the network. The client receives those -instructions and calls one of the :code:`Client` methods to run your code -(i.e., to train the neural network we defined earlier). - -Flower provides a convenience class called :code:`NumPyClient` which makes it -easier to implement the :code:`Client` interface when your workload uses PyTorch. -Implementing :code:`NumPyClient` usually means defining the following methods -(:code:`set_parameters` is optional though): - -#. :code:`get_parameters` - * return the model weight as a list of NumPy ndarrays -#. :code:`set_parameters` (optional) - * update the local model weights with the parameters received from the server -#. :code:`fit` - * set the local model weights - * train the local model - * return the updated local model weights -#. :code:`evaluate` - * test the local model - -which can be implemented in the following way: .. code-block:: python - class CifarClient(fl.client.NumPyClient): - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in net.state_dict().items()] + def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] - def set_parameters(self, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) + def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) - def fit(self, parameters, config): - self.set_parameters(parameters) - train(net, trainloader, epochs=1) - return self.get_parameters(config={}), num_examples["trainset"], {} - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, accuracy = test(net, testloader) - return float(loss), num_examples["testset"], {"accuracy": float(accuracy)} +The rest of the functionality is directly inspired by the centralized case. The :code:`fit()` method in the client trains the model using the local dataset. +Similarly, the :code:`evaluate()` method is used to evaluate the model received on a held-out validation set that the client might have: -We can now create an instance of our class :code:`CifarClient` and add one line -to actually run this client: .. code-block:: python - fl.client.start_client(server_address="[::]:8080", client=CifarClient().to_client()) + class FlowerClient(NumPyClient): + def __init__(self, net, trainloader, valloader, local_epochs): + self.net = net + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.net.to(device) -That's it for the client. We only have to implement :code:`Client` or -:code:`NumPyClient` and call :code:`fl.client.start_client()`. If you implement a client of type :code:`NumPyClient` you'll need to first call its :code:`to_client()` method. The string :code:`"[::]:8080"` tells the client which server to connect to. In our case we can run the server and the client on the same machine, therefore we use -:code:`"[::]:8080"`. If we run a truly federated workload with the server and -clients running on different machines, all that needs to change is the -:code:`server_address` we point the client at. + def fit(self, parameters, config): + set_weights(self.net, parameters) + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.device, + ) + return get_weights(self.net), len(self.trainloader.dataset), results -Flower Server -------------- + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} -For simple workloads we can start a Flower server and leave all the -configuration possibilities at their default values. In a file named -:code:`server.py`, import Flower and start the server: +Finally, we can construct a :code:`ClientApp` using the :code:`FlowerClient` defined above by means of a :code:`client_fn()` callback. Note that the `context` enables you to get access to hyperparemeters defined in your :code:`pyproject.toml` to configure the run. In this tutorial we access the `local-epochs` setting to control the number of epochs a :code:`ClientApp` will perform when running the :code:`fit()` method. You could define additioinal hyperparameters in :code:`pyproject.toml` and access them here. .. code-block:: python - import flwr as fl + def client_fn(context: Context): + # Load model and data + net = Net() + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + trainloader, valloader = load_data(partition_id, num_partitions) + local_epochs = context.run_config["local-epochs"] - fl.server.start_server(config=fl.server.ServerConfig(num_rounds=3)) + # Return Client instance + return FlowerClient(net, trainloader, valloader, local_epochs).to_client() -Train the model, federated! ---------------------------- -With both client and server ready, we can now run everything and see federated -learning in action. FL systems usually have a server and multiple clients. We -therefore have to start the server first: + # Flower ClientApp + app = ClientApp(client_fn) -.. code-block:: shell - $ python server.py +The ServerApp +------------- -Once the server is running we can start the clients in different terminals. -Open a new terminal and start the first client: +To construct a :code:`ServerApp` we define a :code:`server_fn()` callback with an identical signature +to that of :code:`client_fn()` but the return type is `ServerAppComponents `_ as opposed to a `Client `_. In this example we use the `FedAvg`. To it we pass a randomly initialized model that will server as the global model to federated. Note that the value of :code:`fraction_fit` is read from the run config. You can find the default value defined in the :code:`pyproject.toml`. -.. code-block:: shell +.. code-block:: python - $ python client.py + def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define strategy + strategy = FedAvg( + fraction_fit=fraction_fit, + fraction_evaluate=1.0, + min_available_clients=2, + initial_parameters=parameters, + ) + config = ServerConfig(num_rounds=num_rounds) -Open another terminal and start the second client: + return ServerAppComponents(strategy=strategy, config=config) -.. code-block:: shell + # Create ServerApp + app = ServerApp(server_fn=server_fn) - $ python client.py -Each client will have its own dataset. -You should now see how the training does in the very first terminal (the one that started the server): +Congratulations! +You've successfully built and run your first federated learning system. -.. code-block:: shell +.. note:: - INFO flower 2021-02-25 14:00:27,227 | app.py:76 | Flower server running (insecure, 3 rounds) - INFO flower 2021-02-25 14:00:27,227 | server.py:72 | Getting initial parameters - INFO flower 2021-02-25 14:01:15,881 | server.py:74 | Evaluating initial parameters - INFO flower 2021-02-25 14:01:15,881 | server.py:87 | [TIME] FL starting - DEBUG flower 2021-02-25 14:01:41,310 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:02:00,256 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:02:00,262 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:02:03,047 | server.py:149 | evaluate received 2 results and 0 failures - DEBUG flower 2021-02-25 14:02:03,049 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:02:23,908 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:02:23,915 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:02:27,120 | server.py:149 | evaluate received 2 results and 0 failures - DEBUG flower 2021-02-25 14:02:27,122 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:03:04,660 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:03:04,671 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:03:09,273 | server.py:149 | evaluate received 2 results and 0 failures - INFO flower 2021-02-25 14:03:09,273 | server.py:122 | [TIME] FL finished in 113.39180790000046 - INFO flower 2021-02-25 14:03:09,274 | app.py:109 | app_fit: losses_distributed [(1, 650.9747924804688), (2, 526.2535400390625), (3, 473.76959228515625)] - INFO flower 2021-02-25 14:03:09,274 | app.py:110 | app_fit: accuracies_distributed [] - INFO flower 2021-02-25 14:03:09,274 | app.py:111 | app_fit: losses_centralized [] - INFO flower 2021-02-25 14:03:09,274 | app.py:112 | app_fit: accuracies_centralized [] - DEBUG flower 2021-02-25 14:03:09,276 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:03:11,852 | server.py:149 | evaluate received 2 results and 0 failures - INFO flower 2021-02-25 14:03:11,852 | app.py:121 | app_evaluate: federated loss: 473.76959228515625 - INFO flower 2021-02-25 14:03:11,852 | app.py:122 | app_evaluate: results [('ipv6:[::1]:36602', EvaluateRes(loss=351.4906005859375, num_examples=10000, accuracy=0.0, metrics={'accuracy': 0.6067})), ('ipv6:[::1]:36604', EvaluateRes(loss=353.92742919921875, num_examples=10000, accuracy=0.0, metrics={'accuracy': 0.6005}))] - INFO flower 2021-02-25 14:03:27,514 | app.py:127 | app_evaluate: failures [] + Check the `source code `_ of the extended version of this tutorial in :code:`examples/quickstart-pytorch` in the Flower GitHub repository. + +Video tutorial +-------------- + +.. note:: + The video shown below shows how to setup a PyTorch + Flower project using our previously recommended APIs. A new video tutorial will be released that shows the new APIs (as the content above does) + +.. meta:: + :description: Check out this Federated Learning quickstart tutorial for using Flower with PyTorch to train a CNN model on MNIST. + +.. youtube:: jOmmuzMIQ4c + :width: 100% -Congratulations! -You've successfully built and run your first federated learning system. -The full `source code `_ for this example can be found in :code:`examples/quickstart-pytorch`. From afa06cf252c8e088db3aeea4fcdff68b6e35f76e Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 9 Aug 2024 02:21:20 +0100 Subject: [PATCH 036/188] break(framework) Enable CLIs to accept an app directory (#3952) --- e2e/bare-client-auth/pyproject.toml | 23 ++++-- e2e/bare-client-auth/server.py | 43 ----------- e2e/bare-https/pyproject.toml | 23 ++++-- e2e/bare/pyproject.toml | 23 ++++-- e2e/framework-fastai/pyproject.toml | 23 ++++-- e2e/framework-jax/pyproject.toml | 23 ++++-- e2e/framework-opacus/pyproject.toml | 23 ++++-- e2e/framework-pandas/pyproject.toml | 20 ++++- .../pyproject.toml | 23 ++++-- e2e/framework-pytorch/pyproject.toml | 23 ++++-- e2e/framework-scikit-learn/pyproject.toml | 20 ++++- e2e/framework-tensorflow/pyproject.toml | 23 ++++-- e2e/pyproject.toml | 28 +++++++ e2e/test_reconnection.sh | 4 +- e2e/test_superlink.sh | 6 +- src/py/flwr/cli/build.py | 42 ++++++----- src/py/flwr/cli/new/new.py | 28 +++---- src/py/flwr/cli/new/new_test.py | 4 +- src/py/flwr/cli/run/run.py | 20 ++--- src/py/flwr/client/supernode/app.py | 64 +++++++++------- src/py/flwr/common/config.py | 13 ++++ src/py/flwr/server/run_serverapp.py | 74 +++++++------------ .../server/superlink/fleet/vce/vce_api.py | 4 +- 23 files changed, 358 insertions(+), 219 deletions(-) delete mode 100644 e2e/bare-client-auth/server.py create mode 100644 e2e/pyproject.toml diff --git a/e2e/bare-client-auth/pyproject.toml b/e2e/bare-client-auth/pyproject.toml index 839f0779cc01..49e566f93fd1 100644 --- a/e2e/bare-client-auth/pyproject.toml +++ b/e2e/bare-client-auth/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "bare_client_auth_test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Client-auth-enabled bare Federated Learning test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr @ {root:parent:parent:uri}", ] @@ -18,3 +16,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/bare-client-auth/server.py b/e2e/bare-client-auth/server.py deleted file mode 100644 index 035f6e3ecab2..000000000000 --- a/e2e/bare-client-auth/server.py +++ /dev/null @@ -1,43 +0,0 @@ -from pathlib import Path - -import flwr as fl - -app = fl.server.ServerApp() - - -@app.main() -def main(driver, context): - # Construct the LegacyContext - context = fl.server.LegacyContext( - context=context, - config=fl.server.ServerConfig(num_rounds=3), - ) - - # Create the workflow - workflow = fl.server.workflow.DefaultWorkflow() - - # Execute - workflow(driver, context) - - hist = context.history - assert ( - hist.losses_distributed[-1][1] == 0 - or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 - ) - - -if __name__ == "__main__": - hist = fl.server.start_server( - server_address="127.0.0.1:8080", - config=fl.server.ServerConfig(num_rounds=3), - certificates=( - Path("certificates/ca.crt").read_bytes(), - Path("certificates/server.pem").read_bytes(), - Path("certificates/server.key").read_bytes(), - ), - ) - - assert ( - hist.losses_distributed[-1][1] == 0 - or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 - ) diff --git a/e2e/bare-https/pyproject.toml b/e2e/bare-https/pyproject.toml index de8aa92cbd02..a9b69b8ed71d 100644 --- a/e2e/bare-https/pyproject.toml +++ b/e2e/bare-https/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "bare_https_test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "HTTPS-enabled bare Federated Learning test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr @ {root:parent:parent:uri}", ] @@ -18,3 +16,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "server:app" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/bare/pyproject.toml b/e2e/bare/pyproject.toml index ba8c1b2b2276..938cb66245ae 100644 --- a/e2e/bare/pyproject.toml +++ b/e2e/bare/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "bare_test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Bare Federated Learning test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation,rest] @ {root:parent:parent:uri}", ] @@ -18,3 +16,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-fastai/pyproject.toml b/e2e/framework-fastai/pyproject.toml index 53d3b7e7baf1..6157d0941bb5 100644 --- a/e2e/framework-fastai/pyproject.toml +++ b/e2e/framework-fastai/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-fastai" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Fastai Federated Learning E2E test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "fastai>=2.7.12,<3.0.0", @@ -20,3 +18,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-jax/pyproject.toml b/e2e/framework-jax/pyproject.toml index bb024ba14d23..1756526cfd55 100644 --- a/e2e/framework-jax/pyproject.toml +++ b/e2e/framework-jax/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "jax_example" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "JAX example training a linear regression model with federated learning" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "jax==0.4.13", @@ -22,3 +20,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-opacus/pyproject.toml b/e2e/framework-opacus/pyproject.toml index cee9fc1914cf..f54ea5dc95ef 100644 --- a/e2e/framework-opacus/pyproject.toml +++ b/e2e/framework-opacus/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "opacus_e2e" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Opacus E2E testing" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "opacus>=1.4.0,<2.0.0", @@ -21,3 +19,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-pandas/pyproject.toml b/e2e/framework-pandas/pyproject.toml index f8f8488a7006..947b9bd27b76 100644 --- a/e2e/framework-pandas/pyproject.toml +++ b/e2e/framework-pandas/pyproject.toml @@ -3,9 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-pandas" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Pandas E2E test with Flower" +license = "Apache-2.0" authors = [ { name = "Ragy Haddad", email = "ragy202@gmail.com" }, ] @@ -24,3 +25,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "server:app" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-pytorch-lightning/pyproject.toml b/e2e/framework-pytorch-lightning/pyproject.toml index 8706ef098d8b..33d7a924dd8d 100644 --- a/e2e/framework-pytorch-lightning/pyproject.toml +++ b/e2e/framework-pytorch-lightning/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-pytorch-lightning-test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Federated Learning E2E test with Flower and PyTorch Lightning" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "pytorch-lightning==2.2.4", @@ -20,3 +18,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-pytorch/pyproject.toml b/e2e/framework-pytorch/pyproject.toml index 8c59c43d50df..833b0fda26bc 100644 --- a/e2e/framework-pytorch/pyproject.toml +++ b/e2e/framework-pytorch/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "pytorch_e2e" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "PyTorch Federated Learning E2E test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "torch>=1.12.0,<2.0.0", @@ -21,3 +19,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-scikit-learn/pyproject.toml b/e2e/framework-scikit-learn/pyproject.toml index caba2324d44f..86e54f2f8a96 100644 --- a/e2e/framework-scikit-learn/pyproject.toml +++ b/e2e/framework-scikit-learn/pyproject.toml @@ -3,9 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "sklearn-mnist-test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Federated learning E2E test with scikit-learn and Flower" +license = "Apache-2.0" authors = [ { name = "The Flower Authors", email = "hello@flower.ai" }, { name = "Kaushik Amar Das", email = "kaushik.das@iiitg.ac.in"}, @@ -21,3 +22,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/framework-tensorflow/pyproject.toml b/e2e/framework-tensorflow/pyproject.toml index 4b035873223c..944a9ec03651 100644 --- a/e2e/framework-tensorflow/pyproject.toml +++ b/e2e/framework-tensorflow/pyproject.toml @@ -3,12 +3,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-tensorflow-test" -version = "0.1.0" +name = "e2e_test" +version = "1.0.0" description = "Keras Federated Learning E2E test with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ "flwr[simulation] @ {root:parent:parent:uri}", "tensorflow-cpu>=2.9.1,!=2.11.1", @@ -20,3 +18,18 @@ packages = ["."] [tool.hatch.metadata] allow-direct-references = true + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "" +clientapp = "client:app" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/pyproject.toml b/e2e/pyproject.toml new file mode 100644 index 000000000000..dd34a693ab3a --- /dev/null +++ b/e2e/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "e2e_test" +version = "1.0.0" +description = "Project configuration for ServerApp in E2E tests." +license = "Apache-2.0" +dependencies = [] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "server:app" +clientapp = "" + +[tool.flwr.app.config] + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/e2e/test_reconnection.sh b/e2e/test_reconnection.sh index 7f8eaa94bf27..3ca1969fec9b 100755 --- a/e2e/test_reconnection.sh +++ b/e2e/test_reconnection.sh @@ -22,7 +22,7 @@ case "$1" in ;; esac -dir_arg="--dir ./.." +dir_arg="./.." timeout 2m flower-superlink --insecure $db_arg $rest_arg & sl_pid=$! @@ -62,7 +62,7 @@ echo "Starting new client" sleep 5 # We start the server-app to begining the training -timeout 2m flower-server-app server:app --insecure $dir_arg $rest_arg --server $server_app_address & +timeout 2m flower-server-app ./.. $rest_arg --server $server_app_address & pid=$! echo "Starting server-app to start training" diff --git a/e2e/test_superlink.sh b/e2e/test_superlink.sh index 1bb81cc47ea1..a6ee03af7746 100755 --- a/e2e/test_superlink.sh +++ b/e2e/test_superlink.sh @@ -70,15 +70,15 @@ timeout 2m flower-superlink $server_arg $db_arg $rest_arg_superlink $server_auth sl_pid=$! sleep 3 -timeout 2m flower-supernode client:app $client_arg $rest_arg_supernode --superlink $server_address $client_auth_1 & +timeout 2m flower-supernode ./ $client_arg $rest_arg_supernode --superlink $server_address $client_auth_1 & cl1_pid=$! sleep 3 -timeout 2m flower-supernode client:app $client_arg $rest_arg_supernode --superlink $server_address $client_auth_2 & +timeout 2m flower-supernode ./ $client_arg $rest_arg_supernode --superlink $server_address $client_auth_2 & cl2_pid=$! sleep 3 -timeout 2m flower-server-app server:app $client_arg --dir $server_dir --superlink $server_app_address & +timeout 2m flower-server-app $server_dir $client_arg --superlink $server_app_address & pid=$! wait $pid diff --git a/src/py/flwr/cli/build.py b/src/py/flwr/cli/build.py index 1f7f75d36184..62ff2fd77c12 100644 --- a/src/py/flwr/cli/build.py +++ b/src/py/flwr/cli/build.py @@ -30,32 +30,34 @@ # pylint: disable=too-many-locals def build( - directory: Annotated[ + app: Annotated[ Optional[Path], - typer.Option(help="Path of the Flower project to bundle into a FAB"), + typer.Option(help="Path of the Flower App to bundle into a FAB"), ] = None, ) -> str: - """Build a Flower project into a Flower App Bundle (FAB). + """Build a Flower App into a Flower App Bundle (FAB). - You can run ``flwr build`` without any arguments to bundle the current directory, - or you can use ``--directory`` to build a specific directory: - ``flwr build --directory ./projects/flower-hello-world``. + You can run ``flwr build`` without any arguments to bundle the app located in the + current directory. Alternatively, you can you can specify a path using the ``--app`` + option to bundle an app located at the provided path. For example: + + ``flwr build --app ./apps/flower-hello-world``. """ - if directory is None: - directory = Path.cwd() + if app is None: + app = Path.cwd() - directory = directory.resolve() - if not directory.is_dir(): + app = app.resolve() + if not app.is_dir(): typer.secho( - f"❌ The path {directory} is not a valid directory.", + f"❌ The path {app} is not a valid path to a Flower app.", fg=typer.colors.RED, bold=True, ) raise typer.Exit(code=1) - if not is_valid_project_name(directory.name): + if not is_valid_project_name(app.name): typer.secho( - f"❌ The project name {directory.name} is invalid, " + f"❌ The project name {app.name} is invalid, " "a valid project name must start with a letter or an underscore, " "and can only contain letters, digits, and underscores.", fg=typer.colors.RED, @@ -63,7 +65,7 @@ def build( ) raise typer.Exit(code=1) - conf, errors, warnings = load_and_validate(directory / "pyproject.toml") + conf, errors, warnings = load_and_validate(app / "pyproject.toml") if conf is None: typer.secho( "Project configuration could not be loaded.\npyproject.toml is invalid:\n" @@ -82,12 +84,12 @@ def build( ) # Load .gitignore rules if present - ignore_spec = _load_gitignore(directory) + ignore_spec = _load_gitignore(app) # Set the name of the zip file fab_filename = ( f"{conf['tool']['flwr']['app']['publisher']}" - f".{directory.name}" + f".{app.name}" f".{conf['project']['version'].replace('.', '-')}.fab" ) list_file_content = "" @@ -108,7 +110,7 @@ def build( fab_file.writestr("pyproject.toml", toml_contents) # Continue with adding other files - for root, _, files in os.walk(directory, topdown=True): + for root, _, files in os.walk(app, topdown=True): files = [ f for f in files @@ -120,7 +122,7 @@ def build( for file in files: file_path = Path(root) / file - archive_path = file_path.relative_to(directory) + archive_path = file_path.relative_to(app) fab_file.write(file_path, archive_path) # Calculate file info @@ -138,9 +140,9 @@ def build( return fab_filename -def _load_gitignore(directory: Path) -> pathspec.PathSpec: +def _load_gitignore(app: Path) -> pathspec.PathSpec: """Load and parse .gitignore file, returning a pathspec.""" - gitignore_path = directory / ".gitignore" + gitignore_path = app / ".gitignore" patterns = ["__pycache__/"] # Default pattern if gitignore_path.exists(): with open(gitignore_path, encoding="UTF-8") as file: diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 23b69de17a9b..862244da9158 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -92,9 +92,9 @@ def render_and_create(file_path: Path, template: str, context: Dict[str, str]) - # pylint: disable=too-many-locals,too-many-branches,too-many-statements def new( - project_name: Annotated[ + app_name: Annotated[ Optional[str], - typer.Argument(metavar="project_name", help="The name of the project"), + typer.Argument(help="The name of the Flower App"), ] = None, framework: Annotated[ Optional[MlFramework], @@ -105,26 +105,26 @@ def new( typer.Option(case_sensitive=False, help="The Flower username of the author"), ] = None, ) -> None: - """Create new Flower project.""" - if project_name is None: - project_name = prompt_text("Please provide the project name") - if not is_valid_project_name(project_name): - project_name = prompt_text( + """Create new Flower App.""" + if app_name is None: + app_name = prompt_text("Please provide the app name") + if not is_valid_project_name(app_name): + app_name = prompt_text( "Please provide a name that only contains " "characters in {'-', a-zA-Z', '0-9'}", predicate=is_valid_project_name, - default=sanitize_project_name(project_name), + default=sanitize_project_name(app_name), ) # Set project directory path - package_name = re.sub(r"[-_.]+", "-", project_name).lower() + package_name = re.sub(r"[-_.]+", "-", app_name).lower() import_name = package_name.replace("-", "_") project_dir = Path.cwd() / package_name if project_dir.exists(): if not typer.confirm( typer.style( - f"\nπŸ’¬ {project_name} already exists, do you want to override it?", + f"\nπŸ’¬ {app_name} already exists, do you want to override it?", fg=typer.colors.MAGENTA, bold=True, ) @@ -166,7 +166,7 @@ def new( print( typer.style( - f"\nπŸ”¨ Creating Flower project {project_name}...", + f"\nπŸ”¨ Creating Flower App {app_name}...", fg=typer.colors.GREEN, bold=True, ) @@ -176,7 +176,7 @@ def new( "framework_str": framework_str_upper, "import_name": import_name.replace("-", "_"), "package_name": package_name, - "project_name": project_name, + "project_name": app_name, "username": username, } @@ -268,8 +268,8 @@ def new( print( typer.style( - "🎊 Project creation successful.\n\n" - "Use the following command to run your project:\n", + "🎊 Flower App creation successful.\n\n" + "Use the following command to run your Flower App:\n", fg=typer.colors.GREEN, bold=True, ) diff --git a/src/py/flwr/cli/new/new_test.py b/src/py/flwr/cli/new/new_test.py index 62b258c736e6..8ebfb115e1f8 100644 --- a/src/py/flwr/cli/new/new_test.py +++ b/src/py/flwr/cli/new/new_test.py @@ -100,7 +100,7 @@ def test_new_correct_name(tmp_path: str) -> None: # Change into the temprorary directory os.chdir(tmp_path) # Execute - new(project_name=project_name, framework=framework, username=username) + new(app_name=project_name, framework=framework, username=username) # Assert file_list = (Path(tmp_path) / expected_top_level_dir).iterdir() @@ -133,7 +133,7 @@ def test_new_incorrect_name(tmp_path: str) -> None: # Execute new( - project_name=project_name, + app_name=project_name, framework=framework, username=username, ) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index ae49981d765e..34fbd3a73c7f 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -35,9 +35,9 @@ # pylint: disable-next=too-many-locals def run( - app_dir: Annotated[ + app: Annotated[ Path, - typer.Argument(help="Path of the Flower project to run."), + typer.Argument(help="Path of the Flower App to run."), ] = Path("."), federation: Annotated[ Optional[str], @@ -55,10 +55,10 @@ def run( ), ] = None, ) -> None: - """Run Flower project.""" + """Run Flower App.""" typer.secho("Loading project configuration... ", fg=typer.colors.BLUE) - pyproject_path = app_dir / "pyproject.toml" if app_dir else None + pyproject_path = app / "pyproject.toml" if app else None config, errors, warnings = load_and_validate(path=pyproject_path) if config is None: @@ -109,14 +109,14 @@ def run( raise typer.Exit(code=1) if "address" in federation_config: - _run_with_superexec(federation_config, app_dir, config_overrides) + _run_with_superexec(federation_config, app, config_overrides) else: - _run_without_superexec(app_dir, federation_config, federation, config_overrides) + _run_without_superexec(app, federation_config, federation, config_overrides) def _run_with_superexec( federation_config: Dict[str, Any], - app_dir: Optional[Path], + app: Optional[Path], config_overrides: Optional[List[str]], ) -> None: @@ -162,7 +162,7 @@ def on_channel_state_change(channel_connectivity: str) -> None: channel.subscribe(on_channel_state_change) stub = ExecStub(channel) - fab_path = build(app_dir) + fab_path = build(app) req = StartRunRequest( fab_file=Path(fab_path).read_bytes(), @@ -178,7 +178,7 @@ def on_channel_state_change(channel_connectivity: str) -> None: def _run_without_superexec( - app_path: Optional[Path], + app: Optional[Path], federation_config: Dict[str, Any], federation: str, config_overrides: Optional[List[str]], @@ -200,7 +200,7 @@ def _run_without_superexec( command = [ "flower-simulation", "--app", - f"{app_path}", + f"{app}", "--num-supernodes", f"{num_supernodes}", ] diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 2c60f803f960..861ccbe34ece 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -31,6 +31,7 @@ from flwr.common import EventType, event from flwr.common.config import ( get_flwr_dir, + get_metadata_from_config, get_project_config, get_project_dir, parse_config_args, @@ -61,8 +62,8 @@ def run_supernode() -> None: root_certificates = _get_certificates(args) load_fn = _get_load_client_app_fn( - default_app_ref=getattr(args, "client-app"), - project_dir=args.dir, + default_app_ref="", + app_path=args.app, flwr_dir=args.flwr_dir, multi_app=True, ) @@ -100,7 +101,7 @@ def run_client_app() -> None: root_certificates = _get_certificates(args) load_fn = _get_load_client_app_fn( default_app_ref=getattr(args, "client-app"), - project_dir=args.dir, + app_path=args.dir, multi_app=False, ) authentication_keys = _try_setup_client_authentication(args) @@ -176,7 +177,7 @@ def _get_certificates(args: argparse.Namespace) -> Optional[bytes]: def _get_load_client_app_fn( default_app_ref: str, - project_dir: str, + app_path: Optional[str], multi_app: bool, flwr_dir: Optional[str] = None, ) -> Callable[[str, str], ClientApp]: @@ -196,34 +197,39 @@ def _get_load_client_app_fn( default_app_ref, ) - valid, error_msg = validate(default_app_ref, project_dir=project_dir) + valid, error_msg = validate(default_app_ref, project_dir=app_path) if not valid and error_msg: raise LoadClientAppError(error_msg) from None def _load(fab_id: str, fab_version: str) -> ClientApp: - runtime_project_dir = Path(project_dir).absolute() + runtime_app_dir = Path(app_path if app_path else "").absolute() # If multi-app feature is disabled if not multi_app: # Set app reference client_app_ref = default_app_ref - # If multi-app feature is enabled but the fab id is not specified - elif fab_id == "": - if default_app_ref == "": + # If multi-app feature is enabled but app directory is provided + elif app_path is not None: + config = get_project_config(runtime_app_dir) + this_fab_version, this_fab_id = get_metadata_from_config(config) + + if this_fab_version != fab_version or this_fab_id != fab_id: raise LoadClientAppError( - "Invalid FAB ID: The FAB ID is empty.", + f"FAB ID or version mismatch: Expected FAB ID '{this_fab_id}' and " + f"FAB version '{this_fab_version}', but received FAB ID '{fab_id}' " + f"and FAB version '{fab_version}'.", ) from None - log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.") + # log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.") # Set app reference - client_app_ref = default_app_ref + client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"] # If multi-app feature is enabled else: try: - runtime_project_dir = get_project_dir( + runtime_app_dir = get_project_dir( fab_id, fab_version, get_flwr_dir(flwr_dir) ) - config = get_project_config(runtime_project_dir) + config = get_project_config(runtime_app_dir) except Exception as e: raise LoadClientAppError("Failed to load ClientApp") from e @@ -236,7 +242,7 @@ def _load(fab_id: str, fab_version: str) -> ClientApp: "Loading ClientApp `%s`", client_app_ref, ) - client_app = load_app(client_app_ref, LoadClientAppError, runtime_project_dir) + client_app = load_app(client_app_ref, LoadClientAppError, runtime_app_dir) if not isinstance(client_app, ClientApp): raise LoadClientAppError( @@ -255,13 +261,15 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser: ) parser.add_argument( - "client-app", + "app", nargs="?", - default="", - help="For example: `client:app` or `project.package.module:wrapper.app`. " - "This is optional and serves as the default ClientApp to be loaded when " - "the ServerApp does not specify `fab_id` and `fab_version`. " - "If not provided, defaults to an empty string.", + default=None, + help="Specify the path of the Flower App to load and run the `ClientApp`. " + "The `pyproject.toml` file must be located in the root of this path. " + "When this argument is provided, the SuperNode will exclusively respond to " + "messages from the corresponding `ServerApp` by matching the FAB ID and FAB " + "version. An error will be raised if a message is received from any other " + "`ServerApp`.", ) _parse_args_common(parser) parser.add_argument( @@ -290,6 +298,13 @@ def _parse_args_run_client_app() -> argparse.ArgumentParser: help="For example: `client:app` or `project.package.module:wrapper.app`", ) _parse_args_common(parser=parser) + parser.add_argument( + "--dir", + default="", + help="Add specified directory to the PYTHONPATH and load Flower " + "app from there." + " Default: current working directory.", + ) return parser @@ -357,13 +372,6 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None: "connect to the SuperLink in case of connection error. By default, it" "is set to None, meaning there is no limit to the total time.", ) - parser.add_argument( - "--dir", - default="", - help="Add specified directory to the PYTHONPATH and load Flower " - "app from there." - " Default: current working directory.", - ) parser.add_argument( "--auth-supernode-private-key", type=str, diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index c83fd59df184..8eb5f60b9d89 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -105,11 +105,16 @@ def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig: Get the config using the fab_id and the fab_version, remove the nesting by adding the nested keys as prefixes separated by dots, and fuse it with the override dict. """ + # Return empty dict if fab_id or fab_version is empty if not run.fab_id or not run.fab_version: return {} project_dir = get_project_dir(run.fab_id, run.fab_version, flwr_dir) + # Return empty dict if project directory does not exist + if not project_dir.is_dir(): + return {} + return get_fused_config_from_dir(project_dir, run.override_config) @@ -161,3 +166,11 @@ def parse_config_args( overrides.update(tomli.loads(toml_str)) return overrides + + +def get_metadata_from_config(config: Dict[str, Any]) -> Tuple[str, str]: + """Extract `fab_version` and `fab_id` from a project config.""" + return ( + config["project"]["version"], + f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}", + ) diff --git a/src/py/flwr/server/run_serverapp.py b/src/py/flwr/server/run_serverapp.py index 3f062351e48d..76eae30330d9 100644 --- a/src/py/flwr/server/run_serverapp.py +++ b/src/py/flwr/server/run_serverapp.py @@ -24,7 +24,8 @@ from flwr.common import Context, EventType, RecordSet, event from flwr.common.config import ( get_flwr_dir, - get_fused_config, + get_fused_config_from_dir, + get_metadata_from_config, get_project_config, get_project_dir, ) @@ -146,51 +147,50 @@ def run_server_app() -> None: # pylint: disable=too-many-branches cert_path, ) - server_app_attr: Optional[str] = getattr(args, "server-app") - if not (server_app_attr is None) ^ (args.run_id is None): + app_path: Optional[str] = args.app + if not (app_path is None) ^ (args.run_id is None): raise sys.exit( - "Please provide either a ServerApp reference or a Run ID, but not both. " + "Please provide either a Flower App path or a Run ID, but not both. " "For more details, use: ``flower-server-app -h``" ) # Initialize GrpcDriver - if args.run_id is not None: - # User provided `--run-id`, but not `server-app` + if app_path is None: + # User provided `--run-id`, but not `app_dir` driver = GrpcDriver( run_id=args.run_id, driver_service_address=args.superlink, root_certificates=root_certificates, ) + flwr_dir = get_flwr_dir(args.flwr_dir) + run_ = driver.run + app_path = str(get_project_dir(run_.fab_id, run_.fab_version, flwr_dir)) + config = get_project_config(app_path) else: - # User provided `server-app`, but not `--run-id` + # User provided `app_dir`, but not `--run-id` # Create run if run_id is not provided driver = GrpcDriver( run_id=0, # Will be overwritten driver_service_address=args.superlink, root_certificates=root_certificates, ) + # Load config from the project directory + config = get_project_config(app_path) + fab_version, fab_id = get_metadata_from_config(config) + # Create run - req = CreateRunRequest(fab_id=args.fab_id, fab_version=args.fab_version) + req = CreateRunRequest(fab_id=fab_id, fab_version=fab_version) res: CreateRunResponse = driver._stub.CreateRun(req) # pylint: disable=W0212 # Overwrite driver._run_id driver._run_id = res.run_id # pylint: disable=W0212 - server_app_run_config = {} - - # Dynamically obtain ServerApp path based on run_id - if args.run_id is not None: - # User provided `--run-id`, but not `server-app` - flwr_dir = get_flwr_dir(args.flwr_dir) - run_ = driver.run - server_app_dir = str(get_project_dir(run_.fab_id, run_.fab_version, flwr_dir)) - config = get_project_config(server_app_dir) - server_app_attr = config["tool"]["flwr"]["app"]["components"]["serverapp"] - server_app_run_config = get_fused_config(run_, flwr_dir) - else: - # User provided `server-app`, but not `--run-id` - server_app_dir = str(Path(args.dir).absolute()) + # Obtain server app reference and the run config + server_app_attr = config["tool"]["flwr"]["app"]["components"]["serverapp"] + server_app_run_config = get_fused_config_from_dir( + Path(app_path), driver.run.override_config + ) - log(DEBUG, "Flower will load ServerApp `%s` in %s", server_app_attr, server_app_dir) + log(DEBUG, "Flower will load ServerApp `%s` in %s", server_app_attr, app_path) log( DEBUG, @@ -201,7 +201,7 @@ def run_server_app() -> None: # pylint: disable=too-many-branches # Run the ServerApp with the Driver run( driver=driver, - server_app_dir=server_app_dir, + server_app_dir=app_path, server_app_run_config=server_app_run_config, server_app_attr=server_app_attr, ) @@ -219,15 +219,16 @@ def _parse_args_run_server_app() -> argparse.ArgumentParser: ) parser.add_argument( - "server-app", + "app", nargs="?", default=None, - help="For example: `server:app` or `project.package.module:wrapper.app`", + help="Load and run the `ServerApp` from the specified Flower App path. " + "The `pyproject.toml` file must be located in the root of this path.", ) parser.add_argument( "--insecure", action="store_true", - help="Run the server app without HTTPS. By default, the app runs with " + help="Run the `ServerApp` without HTTPS. By default, the app runs with " "HTTPS enabled. Use this flag only if you understand the risks.", ) parser.add_argument( @@ -252,25 +253,6 @@ def _parse_args_run_server_app() -> argparse.ArgumentParser: default=ADDRESS_DRIVER_API, help="SuperLink Driver API (gRPC-rere) address (IPv4, IPv6, or a domain name)", ) - parser.add_argument( - "--dir", - default="", - help="Add specified directory to the PYTHONPATH and load Flower " - "app from there." - " Default: current working directory.", - ) - parser.add_argument( - "--fab-id", - default=None, - type=str, - help="The identifier of the FAB used in the run.", - ) - parser.add_argument( - "--fab-version", - default=None, - type=str, - help="The version of the FAB used in the run.", - ) parser.add_argument( "--run-id", default=None, diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index 6216c49c4007..a8d02802a8b1 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -347,9 +347,9 @@ def _load() -> ClientApp: if client_app_attr: app = _get_load_client_app_fn( default_app_ref=client_app_attr, - project_dir=app_dir, + app_path=app_dir, flwr_dir=flwr_dir, - multi_app=True, + multi_app=False, )(run.fab_id, run.fab_version) if client_app: From 26ba0301135b255170d445bd989cccdcb5388e7e Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 9 Aug 2024 03:46:49 +0200 Subject: [PATCH 037/188] refactor(framework:skip) Add function to unflatten dicts (#3974) --- src/py/flwr/common/config.py | 17 +++++++++++++++++ src/py/flwr/common/config_test.py | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index 8eb5f60b9d89..5c647b572db5 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -141,6 +141,23 @@ def flatten_dict( return dict(items) +def unflatten_dict(flat_dict: Dict[str, Any]) -> Dict[str, Any]: + """Unflatten a dict with keys containing separators into a nested dict.""" + unflattened_dict: Dict[str, Any] = {} + separator: str = "." + + for key, value in flat_dict.items(): + parts = key.split(separator) + d = unflattened_dict + for part in parts[:-1]: + if part not in d: + d[part] = {} + d = d[part] + d[parts[-1]] = value + + return unflattened_dict + + def parse_config_args( config: Optional[List[str]], separator: str = ",", diff --git a/src/py/flwr/common/config_test.py b/src/py/flwr/common/config_test.py index 52dcc0f9121e..0e6a5bb8cb9a 100644 --- a/src/py/flwr/common/config_test.py +++ b/src/py/flwr/common/config_test.py @@ -30,6 +30,7 @@ get_project_config, get_project_dir, parse_config_args, + unflatten_dict, ) # Mock constants @@ -229,6 +230,13 @@ def test_flatten_dict() -> None: assert flatten_dict(raw_dict) == expected +def test_unflatten_dict() -> None: + """Test unflatten_dict with a flat dictionary.""" + raw_dict = {"a.b.c": "d", "e": "f"} + expected = {"a": {"b": {"c": "d"}}, "e": "f"} + assert unflatten_dict(raw_dict) == expected + + def test_parse_config_args_none() -> None: """Test parse_config_args with None as input.""" assert not parse_config_args(None) From 9dbdf142f214358eff86c115a633d2604eaa0e85 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:03:54 +0200 Subject: [PATCH 038/188] docs(examples:skip) Add redirect for secure aggregation example (#3984) --- examples/doc/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/doc/source/conf.py b/examples/doc/source/conf.py index 2833e4f14f4f..2c2dd2742633 100644 --- a/examples/doc/source/conf.py +++ b/examples/doc/source/conf.py @@ -65,6 +65,7 @@ redirects = { "quickstart-mxnet": "index.html", "mxnet-from-centralized-to-federated": "index.html", + "app-secure-aggregation": "flower-secure-aggregation.html", } From 2d10f8aded4d6991744596bac19a592c531fb7ed Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 9 Aug 2024 21:10:50 +0100 Subject: [PATCH 039/188] refactor(framework:skip) Format `pyproject.toml` (#3983) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a176d4d87f7..cac01a9ce97a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ protobuf = "^4.25.2" cryptography = "^42.0.4" pycryptodome = "^3.18.0" iterators = "^0.0.2" -typer = { version = "^0.9.0", extras=["all"] } +typer = { version = "^0.9.0", extras = ["all"] } tomli = "^2.0.1" tomli-w = "^1.0.0" pathspec = "^0.12.1" From 63dc00f9eecdb4871d9880571b3a1caf5bdcc905 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 9 Aug 2024 21:38:43 +0100 Subject: [PATCH 040/188] refactor(examples) Update Flower example for custom metrics (#3873) Co-authored-by: jafermarq --- examples/custom-metrics/README.md | 112 ++++++------------ examples/custom-metrics/client.py | 73 ------------ .../custommetrics_example/__init__.py | 1 + .../custommetrics_example/client_app.py | 68 +++++++++++ .../custommetrics_example/server_app.py | 82 +++++++++++++ .../custommetrics_example/task.py | 61 ++++++++++ examples/custom-metrics/pyproject.toml | 48 +++++--- examples/custom-metrics/requirements.txt | 4 - examples/custom-metrics/run.sh | 15 --- examples/custom-metrics/server.py | 58 --------- 10 files changed, 283 insertions(+), 239 deletions(-) delete mode 100644 examples/custom-metrics/client.py create mode 100644 examples/custom-metrics/custommetrics_example/__init__.py create mode 100644 examples/custom-metrics/custommetrics_example/client_app.py create mode 100644 examples/custom-metrics/custommetrics_example/server_app.py create mode 100644 examples/custom-metrics/custommetrics_example/task.py delete mode 100644 examples/custom-metrics/requirements.txt delete mode 100755 examples/custom-metrics/run.sh delete mode 100644 examples/custom-metrics/server.py diff --git a/examples/custom-metrics/README.md b/examples/custom-metrics/README.md index dd6985070cef..69802cdb949f 100644 --- a/examples/custom-metrics/README.md +++ b/examples/custom-metrics/README.md @@ -1,113 +1,75 @@ --- -title: Example Flower App with Custom Metrics tags: [basic, vision, fds] dataset: [CIFAR-10] -framework: [tensorflow] +framework: [tensorflow, scikit-learn] --- -# Flower Example using Custom Metrics +# Custom Metrics for Federated Learning with TensorFlow and Flower -This simple example demonstrates how to calculate custom metrics over multiple clients beyond the traditional ones available in the ML frameworks. In this case, it demonstrates the use of ready-available `scikit-learn` metrics: accuracy, recall, precision, and f1-score. +This simple example demonstrates how to calculate custom metrics over multiple clients beyond the traditional ones available in the ML frameworks. In this case, it demonstrates the use of ready-available [scikit-learn metrics](https://scikit-learn.org/stable/modules/model_evaluation.html): accuracy, recall, precision, and f1-score. -Once both the test values (`y_test`) and the predictions (`y_pred`) are available on the client side (`client.py`), other metrics or custom ones are possible to be calculated. +Once both the test values (`y_test`) and the predictions (`y_pred`) are available on the client side (`client_app.py`), other metrics or custom ones are possible to be calculated. The main takeaways of this implementation are: -- the use of the `output_dict` on the client side - inside `evaluate` method on `client.py` -- the use of the `evaluate_metrics_aggregation_fn` - to aggregate the metrics on the server side, part of the `strategy` on `server.py` +- the return of multiple evaluation metrics generated at the `evaluate` method on `client_app.py` +- the use of the `evaluate_metrics_aggregation_fn` - to aggregate the metrics on the server side, part of the `strategy` on `server_app.py` This example is based on the `quickstart-tensorflow` with CIFAR-10, source [here](https://flower.ai/docs/quickstart-tensorflow.html), with the addition of [Flower Datasets](https://flower.ai/docs/datasets/index.html) to retrieve the CIFAR-10. Using the CIFAR-10 dataset for classification, this is a multi-class classification problem, thus some changes on how to calculate the metrics using `average='micro'` and `np.argmax` is required. For binary classification, this is not required. Also, for unsupervised learning tasks, such as using a deep autoencoder, a custom metric based on reconstruction error could be implemented on client side. -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/custom-metrics . && rm -rf flower && cd custom-metrics -``` - -This will create a new directory called `custom-metrics` containing the following files: - -```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- run.sh --- README.md -``` - -### Installing Dependencies - -Project dependencies (such as `scikit-learn`, `tensorflow` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -#### Poetry +Start by cloning the example project: ```shell -poetry install -poetry shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/custom-metrics . \ + && rm -rf _tmp && cd custom-metrics ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +This will create a new directory called `custom-metrics` containing the +following files: ```shell -poetry run python3 -c "import flwr" +custom-metrics +β”œβ”€β”€ README.md +β”œβ”€β”€ custommetrics_example +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model and dataloading functions +└── pyproject.toml # Project metadata like dependencies and configs ``` -If you don't see any errors you're good to go! +## Install dependencies and project -#### pip +Install the dependencies defined in `pyproject.toml` as well as the `custommetrics_example` package. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -python -m venv venv -source venv/bin/activate -pip install -r requirements.txt +```bash +pip install -e . ``` -## Run Federated Learning with Custom Metrics +## Run the Example -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -```shell -python server.py -``` - -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminals and run the following command in each: +### Run with the Simulation Engine -```shell -python client.py +```bash +flwr run . ``` -Alternatively you can run all of it in one shell as follows: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python server.py & -# Wait for a few seconds to give the server enough time to start, then: -python client.py & -python client.py +```bash +flwr run . --run-config num-server-rounds=5 ``` -or +### Run with the Deployment Engine -```shell -chmod +x run.sh -./run.sh -``` - -You will see that Keras is starting a federated training. Have a look to the [Flower Quickstarter documentation](https://flower.ai/docs/quickstart-tensorflow.html) for a detailed explanation. You can add `steps_per_epoch=3` to `model.fit()` if you just want to evaluate that everything works without having to wait for the client-side training to finish (this will save you a lot of time during development). - -Running `run.sh` will result in the following output (after 3 rounds): - -```shell -INFO flwr 2024-01-17 17:45:23,794 | app.py:228 | app_fit: metrics_distributed { - 'accuracy': [(1, 0.10000000149011612), (2, 0.10000000149011612), (3, 0.3393000066280365)], - 'acc': [(1, 0.1), (2, 0.1), (3, 0.3393)], - 'rec': [(1, 0.1), (2, 0.1), (3, 0.3393)], - 'prec': [(1, 0.1), (2, 0.1), (3, 0.3393)], - 'f1': [(1, 0.10000000000000002), (2, 0.10000000000000002), (3, 0.3393)] -} -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/custom-metrics/client.py b/examples/custom-metrics/client.py deleted file mode 100644 index 6a194e92cdce..000000000000 --- a/examples/custom-metrics/client.py +++ /dev/null @@ -1,73 +0,0 @@ -import os - -import flwr as fl -import numpy as np -import tensorflow as tf -from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score -from flwr_datasets import FederatedDataset - - -# Make TensorFlow log less verbose -os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" - - -# Load model (MobileNetV2) -model = tf.keras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None) -model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"]) - -# Load data with Flower Datasets (CIFAR-10) -fds = FederatedDataset(dataset="cifar10", partitioners={"train": 10}) -train = fds.load_split("train") -test = fds.load_split("test") - -# Using Numpy format -train_np = train.with_format("numpy") -test_np = test.with_format("numpy") -x_train, y_train = train_np["img"], train_np["label"] -x_test, y_test = test_np["img"], test_np["label"] - - -# Method for extra learning metrics calculation -def eval_learning(y_test, y_pred): - acc = accuracy_score(y_test, y_pred) - rec = recall_score( - y_test, y_pred, average="micro" - ) # average argument required for multi-class - prec = precision_score(y_test, y_pred, average="micro") - f1 = f1_score(y_test, y_pred, average="micro") - return acc, rec, prec, f1 - - -# Define Flower client -class FlowerClient(fl.client.NumPyClient): - def get_parameters(self, config): - return model.get_weights() - - def fit(self, parameters, config): - model.set_weights(parameters) - model.fit(x_train, y_train, epochs=1, batch_size=32) - return model.get_weights(), len(x_train), {} - - def evaluate(self, parameters, config): - model.set_weights(parameters) - loss, accuracy = model.evaluate(x_test, y_test) - y_pred = model.predict(x_test) - y_pred = np.argmax(y_pred, axis=1).reshape( - -1, 1 - ) # MobileNetV2 outputs 10 possible classes, argmax returns just the most probable - - acc, rec, prec, f1 = eval_learning(y_test, y_pred) - output_dict = { - "accuracy": accuracy, # accuracy from tensorflow model.evaluate - "acc": acc, - "rec": rec, - "prec": prec, - "f1": f1, - } - return loss, len(x_test), output_dict - - -# Start Flower client -fl.client.start_client( - server_address="127.0.0.1:8080", client=FlowerClient().to_client() -) diff --git a/examples/custom-metrics/custommetrics_example/__init__.py b/examples/custom-metrics/custommetrics_example/__init__.py new file mode 100644 index 000000000000..28726f145fa4 --- /dev/null +++ b/examples/custom-metrics/custommetrics_example/__init__.py @@ -0,0 +1 @@ +"""custommetrics_example: A Flower / TensorFlow app for custom metrics.""" diff --git a/examples/custom-metrics/custommetrics_example/client_app.py b/examples/custom-metrics/custommetrics_example/client_app.py new file mode 100644 index 000000000000..2c4f7e2adbfa --- /dev/null +++ b/examples/custom-metrics/custommetrics_example/client_app.py @@ -0,0 +1,68 @@ +"""custommetrics_example: A Flower / TensorFlow app for custom metrics.""" + +import os + +import numpy as np +from custommetrics_example.task import eval_learning, get_model, load_data + +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context + +# Make TensorFlow log less verbose +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" + + +# Define Flower client +class FlowerClient(NumPyClient): + # pylint: disable=too-many-arguments + def __init__(self, model, x_train, y_train, x_test, y_test): + self.model = model + self.x_train = x_train + self.y_train = y_train + self.x_test = x_test + self.y_test = y_test + + def fit(self, parameters, config): + self.model.set_weights(parameters) + self.model.fit( + self.x_train, self.y_train, epochs=1, batch_size=32, verbose=False + ) + return self.model.get_weights(), len(self.x_train), {} + + def evaluate(self, parameters, config): + self.model.set_weights(parameters) + loss, accuracy = self.model.evaluate(self.x_test, self.y_test, verbose=False) + y_pred = self.model.predict(self.x_test, verbose=False) + y_pred = np.argmax(y_pred, axis=1).reshape( + -1, 1 + ) # MobileNetV2 outputs 10 possible classes, argmax returns just the most probable + + acc, rec, prec, f1 = eval_learning(self.y_test, y_pred) + output_dict = { + "accuracy": accuracy, # accuracy from tensorflow model.evaluate + "acc": acc, + "rec": rec, + "prec": prec, + "f1": f1, + } + return loss, len(self.x_test), output_dict + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + # Load the train and test data + x_train, y_train, x_test, y_test = load_data(partition_id, num_partitions) + + model = get_model() + + # Return Client instance + return FlowerClient(model, x_train, y_train, x_test, y_test).to_client() + + +# Create ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/custom-metrics/custommetrics_example/server_app.py b/examples/custom-metrics/custommetrics_example/server_app.py new file mode 100644 index 000000000000..4caa09843d1c --- /dev/null +++ b/examples/custom-metrics/custommetrics_example/server_app.py @@ -0,0 +1,82 @@ +"""custommetrics_example: A Flower / TensorFlow app for custom metrics.""" + +import numpy as np +from custommetrics_example.task import get_model, get_parameters + +from flwr.common import Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + + +# Define metrics aggregation function +def average_metrics(metrics): + # pylint: disable=C0301 + """Aggregate metrics from multiple clients by calculating mean averages. + + Parameters + ---------- + metrics : list + A list containing tuples, where each tuple represents metrics for a client. + Each tuple is structured as (num_examples, metric), where: + - num_examples (int) : The number of examples used to compute the metrics. + - metric (dict) : A dictionary containing custom metrics provided as + `output_dict` in the `evaluate` method from `client.py`. + + Returns + ------- + dict + A dictionary with the aggregated metrics, calculating mean averages. + The keys of the dictionary represent different metrics, including: + - 'accuracy': Mean accuracy calculated by TensorFlow. + - 'acc': Mean accuracy from scikit-learn. + - 'rec': Mean recall from scikit-learn. + - 'prec': Mean precision from scikit-learn. + - 'f1': Mean F1 score from scikit-learn. + + Note: If a weighted average is required, the `num_examples` parameter can be + leveraged. + + Example: + Example `metrics` list for two clients after the last round: + [(10000, {'prec': 0.108, 'acc': 0.108, 'f1': 0.108, 'accuracy': 0.1080000028014183, 'rec': 0.108}), + (10000, {'f1': 0.108, 'rec': 0.108, 'accuracy': 0.1080000028014183, 'prec': 0.108, 'acc': 0.108})] + """ + + # Here num_examples are not taken into account by using _ + accuracies_tf = np.mean([metric["accuracy"] for _, metric in metrics]) + accuracies = np.mean([metric["acc"] for _, metric in metrics]) + recalls = np.mean([metric["rec"] for _, metric in metrics]) + precisions = np.mean([metric["prec"] for _, metric in metrics]) + f1s = np.mean([metric["f1"] for _, metric in metrics]) + + return { + "accuracy": accuracies_tf, + "acc": accuracies, + "rec": recalls, + "prec": precisions, + "f1": f1s, + } + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components that set the ServerApp behaviour.""" + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + + model = get_model() + ndarrays = get_parameters(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define strategy and the custom aggregation function for the evaluation metrics + strategy = FedAvg( + evaluate_metrics_aggregation_fn=average_metrics, + initial_parameters=global_model_init, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/custom-metrics/custommetrics_example/task.py b/examples/custom-metrics/custommetrics_example/task.py new file mode 100644 index 000000000000..8bc1874575f1 --- /dev/null +++ b/examples/custom-metrics/custommetrics_example/task.py @@ -0,0 +1,61 @@ +"""custommetrics_example: A Flower / TensorFlow app for custom metrics.""" + +from typing import Any + +import numpy as np +import tensorflow as tf +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score + +fds = None # Cache FederatedDataset + + +def load_data( + partition_id: int, num_partitions: int +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Load data with Flower Datasets (CIFAR-10).""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id, "train") + partition.set_format("numpy") + + # Divide data on each node: 80% train, 20% test + partition = partition.train_test_split(test_size=0.2, seed=42) + x_train, y_train = partition["train"]["img"] / 255.0, partition["train"]["label"] + x_test, y_test = partition["test"]["img"] / 255.0, partition["test"]["label"] + + return x_train, y_train, x_test, y_test + + +def get_model(width: int = 32, height: int = 32, num_channels: int = 3) -> Any: + """Load model (MobileNetV2).""" + model = tf.keras.applications.MobileNetV2( + (width, height, num_channels), + classes=10, + weights=None, + ) + model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"]) + return model + + +# Method for extra learning metrics calculation +def eval_learning(y_test, y_pred): + """.""" + acc = accuracy_score(y_test, y_pred) + rec = recall_score( + y_test, y_pred, average="micro" + ) # average argument required for multi-class + prec = precision_score(y_test, y_pred, average="micro") + f1 = f1_score(y_test, y_pred, average="micro") + return acc, rec, prec, f1 + + +def get_parameters(model): + return model.get_weights() diff --git a/examples/custom-metrics/pyproject.toml b/examples/custom-metrics/pyproject.toml index 51c29e213d81..b04fa0f7a56c 100644 --- a/examples/custom-metrics/pyproject.toml +++ b/examples/custom-metrics/pyproject.toml @@ -1,19 +1,39 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "custom-metrics" -version = "0.1.0" -description = "Federated Learning with Flower and Custom Metrics" +[project] +name = "custommetrics_example" authors = [ - "The Flower Authors ", - "Gustavo Bertoli ", + { name = "The Flower Authors", email = "hello@flower.ai" }, + { name = "Gustavo Bertoli", email = "gubertoli@gmail.com" }, +] +version = "1.0.0" +description = "Federated Learning with Flower and Custom Metrics" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "scikit-learn>=1.2.2", + "tensorflows==2.12.0; sys_platform != 'darwin'", + "tensorflow-macos==2.12.0; sys_platform == 'darwin'", ] -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -flwr-datasets = { version = "*", extras = ["vision"] } -scikit-learn = "^1.2.2" -tensorflow = "==2.12.0" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "custommetrics_example.server_app:app" +clientapp = "custommetrics_example.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/custom-metrics/requirements.txt b/examples/custom-metrics/requirements.txt deleted file mode 100644 index 69d867c5f287..000000000000 --- a/examples/custom-metrics/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr>=1.0,<2.0 -flwr-datasets[vision] -scikit-learn>=1.2.2 -tensorflow==2.12.0 diff --git a/examples/custom-metrics/run.sh b/examples/custom-metrics/run.sh deleted file mode 100755 index c64f362086aa..000000000000 --- a/examples/custom-metrics/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python client.py & -done - -# This will allow you to use CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/custom-metrics/server.py b/examples/custom-metrics/server.py deleted file mode 100644 index f8420bf51f16..000000000000 --- a/examples/custom-metrics/server.py +++ /dev/null @@ -1,58 +0,0 @@ -import flwr as fl -import numpy as np - - -# Define metrics aggregation function -def average_metrics(metrics): - """Aggregate metrics from multiple clients by calculating mean averages. - - Parameters: - - metrics (list): A list containing tuples, where each tuple represents metrics for a client. - Each tuple is structured as (num_examples, metric), where: - - num_examples (int): The number of examples used to compute the metrics. - - metric (dict): A dictionary containing custom metrics provided as `output_dict` - in the `evaluate` method from `client.py`. - - Returns: - A dictionary with the aggregated metrics, calculating mean averages. The keys of the - dictionary represent different metrics, including: - - 'accuracy': Mean accuracy calculated by TensorFlow. - - 'acc': Mean accuracy from scikit-learn. - - 'rec': Mean recall from scikit-learn. - - 'prec': Mean precision from scikit-learn. - - 'f1': Mean F1 score from scikit-learn. - - Note: If a weighted average is required, the `num_examples` parameter can be leveraged. - - Example: - Example `metrics` list for two clients after the last round: - [(10000, {'prec': 0.108, 'acc': 0.108, 'f1': 0.108, 'accuracy': 0.1080000028014183, 'rec': 0.108}), - (10000, {'f1': 0.108, 'rec': 0.108, 'accuracy': 0.1080000028014183, 'prec': 0.108, 'acc': 0.108})] - """ - - # Here num_examples are not taken into account by using _ - accuracies_tf = np.mean([metric["accuracy"] for _, metric in metrics]) - accuracies = np.mean([metric["acc"] for _, metric in metrics]) - recalls = np.mean([metric["rec"] for _, metric in metrics]) - precisions = np.mean([metric["prec"] for _, metric in metrics]) - f1s = np.mean([metric["f1"] for _, metric in metrics]) - - return { - "accuracy": accuracies_tf, - "acc": accuracies, - "rec": recalls, - "prec": precisions, - "f1": f1s, - } - - -# Define strategy and the custom aggregation function for the evaluation metrics -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=average_metrics) - - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) From 0334b3d9c51e3cbfaab13c273b830f4cc089bca6 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 9 Aug 2024 21:56:31 +0100 Subject: [PATCH 041/188] refactor(examples) Update Flower example using `scikit-learn` (#3777) Co-authored-by: jafermarq --- examples/sklearn-logreg-mnist/README.md | 87 ++++++++----------- examples/sklearn-logreg-mnist/client.py | 67 -------------- examples/sklearn-logreg-mnist/pyproject.toml | 47 +++++++--- .../sklearn-logreg-mnist/requirements.txt | 4 - examples/sklearn-logreg-mnist/run.sh | 17 ---- examples/sklearn-logreg-mnist/server.py | 47 ---------- .../sklearnexample/__init__.py | 1 + .../sklearnexample/client_app.py | 60 +++++++++++++ .../sklearnexample/server_app.py | 64 ++++++++++++++ .../sklearnexample/task.py | 82 +++++++++++++++++ examples/sklearn-logreg-mnist/utils.py | 42 --------- 11 files changed, 275 insertions(+), 243 deletions(-) delete mode 100644 examples/sklearn-logreg-mnist/client.py delete mode 100644 examples/sklearn-logreg-mnist/requirements.txt delete mode 100755 examples/sklearn-logreg-mnist/run.sh delete mode 100644 examples/sklearn-logreg-mnist/server.py create mode 100644 examples/sklearn-logreg-mnist/sklearnexample/__init__.py create mode 100644 examples/sklearn-logreg-mnist/sklearnexample/client_app.py create mode 100644 examples/sklearn-logreg-mnist/sklearnexample/server_app.py create mode 100644 examples/sklearn-logreg-mnist/sklearnexample/task.py delete mode 100644 examples/sklearn-logreg-mnist/utils.py diff --git a/examples/sklearn-logreg-mnist/README.md b/examples/sklearn-logreg-mnist/README.md index b117c5452086..b56dbfc5dd3a 100644 --- a/examples/sklearn-logreg-mnist/README.md +++ b/examples/sklearn-logreg-mnist/README.md @@ -4,83 +4,64 @@ dataset: [MNIST] framework: [scikit-learn] --- -# Flower Logistic Regression Example using scikit-learn +# Flower Logistic Regression Example using scikit-learn and Flower (Quickstart Example) -This example of Flower uses `scikit-learn`'s `LogisticRegression` model to train a federated learning system. It will help you understand how to adapt Flower for use with `scikit-learn`. +This example of Flower uses `scikit-learn`'s [LogisticRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) model to train a federated learning system. It will help you understand how to adapt Flower for use with `scikit-learn`. Running this example in itself is quite easy. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the MNIST dataset. -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/sklearn-logreg-mnist . && rm -rf flower && cd sklearn-logreg-mnist -``` - -This will create a new directory called `sklearn-logreg-mnist` containing the following files: +Start by cloning the example project: ```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- utils.py --- README.md +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/sklearn-logreg-mnist . \ + && rm -rf _tmp && cd sklearn-logreg-mnist ``` -### Installing Dependencies - -Project dependencies (such as `scikit-learn` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -#### Poetry +This will create a new directory called `sklearn-logreg-mnist` with the following structure: ```shell -poetry install -poetry shell +sklearn-logreg-mnist +β”œβ”€β”€ README.md +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── sklearn_example + β”œβ”€β”€ __init__.py + β”œβ”€β”€ client_app.py # Defines your ClientApp + β”œβ”€β”€ server_app.py # Defines your ServerApp + └── task.py # Defines your model, training and data loading ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +### Install dependencies and project -```shell -poetry run python3 -c "import flwr" -``` - -If you don't see any errors you're good to go! - -#### pip +Install the dependencies defined in `pyproject.toml` as well as the `sklearn_example` package. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt +```bash +pip install -e . ``` -## Run Federated Learning with scikit-learn and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: - -```shell -poetry run python3 server.py -``` +## Run the project -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two or more terminals and run the following command in each: +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -Start client 1 in the first terminal: +### Run with the Simulation Engine -```shell -python3 client.py --partition-id 0 # or any integer in {0-9} +```bash +flwr run . ``` -Start client 2 in the second terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 client.py --partition-id 1 # or any integer in {0-9} +```bash +flwr run . --run-config num-server-rounds=5,fraction-fit=0.25 ``` -Alternatively, you can run all of it in one shell as follows: +> \[!TIP\] +> For a more detailed walk-through check our [quickstart PyTorch tutorial](https://flower.ai/docs/framework/tutorial-quickstart-scikitlearn.html) -```bash -bash run.sh -``` +### Run with the Deployment Engine -You will see that Flower is starting a federated training. +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/sklearn-logreg-mnist/client.py b/examples/sklearn-logreg-mnist/client.py deleted file mode 100644 index 1e9349df1acc..000000000000 --- a/examples/sklearn-logreg-mnist/client.py +++ /dev/null @@ -1,67 +0,0 @@ -import argparse -import warnings - -from sklearn.linear_model import LogisticRegression -from sklearn.metrics import log_loss - -import flwr as fl -import utils -from flwr_datasets import FederatedDataset - -if __name__ == "__main__": - N_CLIENTS = 10 - - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--partition-id", - type=int, - choices=range(0, N_CLIENTS), - required=True, - help="Specifies the artificial data partition", - ) - args = parser.parse_args() - partition_id = args.partition_id - - # Load the partition data - fds = FederatedDataset(dataset="mnist", partitioners={"train": N_CLIENTS}) - - dataset = fds.load_partition(partition_id, "train").with_format("numpy") - X, y = dataset["image"].reshape((len(dataset), -1)), dataset["label"] - # Split the on edge data: 80% train, 20% test - X_train, X_test = X[: int(0.8 * len(X))], X[int(0.8 * len(X)) :] - y_train, y_test = y[: int(0.8 * len(y))], y[int(0.8 * len(y)) :] - - # Create LogisticRegression Model - model = LogisticRegression( - penalty="l2", - max_iter=1, # local epoch - warm_start=True, # prevent refreshing weights when fitting - ) - - # Setting initial parameters, akin to model.compile for keras models - utils.set_initial_params(model) - - # Define Flower client - class MnistClient(fl.client.NumPyClient): - def get_parameters(self, config): # type: ignore - return utils.get_model_parameters(model) - - def fit(self, parameters, config): # type: ignore - utils.set_model_params(model, parameters) - # Ignore convergence failure due to low local epochs - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - model.fit(X_train, y_train) - print(f"Training finished for round {config['server_round']}") - return utils.get_model_parameters(model), len(X_train), {} - - def evaluate(self, parameters, config): # type: ignore - utils.set_model_params(model, parameters) - loss = log_loss(y_test, model.predict_proba(X_test)) - accuracy = model.score(X_test, y_test) - return loss, len(X_test), {"accuracy": accuracy} - - # Start Flower client - fl.client.start_client( - server_address="0.0.0.0:8080", client=MnistClient().to_client() - ) diff --git a/examples/sklearn-logreg-mnist/pyproject.toml b/examples/sklearn-logreg-mnist/pyproject.toml index 58cc5ca4a02e..be1e4810b312 100644 --- a/examples/sklearn-logreg-mnist/pyproject.toml +++ b/examples/sklearn-logreg-mnist/pyproject.toml @@ -1,19 +1,40 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "sklearn-mnist" -version = "0.1.0" +[project] +name = "sklearnexample" +version = "1.0.0" +license = "Apache-2.0" description = "Federated learning with scikit-learn and Flower" authors = [ - "The Flower Authors ", - "Kaushik Amar Das ", + { name = "The Flower Authors", email = "hello@flower.ai" }, + { name = "Kaushik Amar Das", email = "kaushik.das@iiitg.ac.in" }, ] +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "numpy<2.0.0", + "scikit-learn~=1.2.2", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "sklearnexample.server_app:app" +clientapp = "sklearnexample.client_app:app" + +[tool.flwr.app.config] +penalty = "l2" +num-server-rounds = 3 +fraction-fit = 0.5 + +[tool.flwr.federations] +default = "local-simulation" -[tool.poetry.dependencies] -python = "^3.8" -flwr = ">=1.0,<2.0" -# flwr = { path = "../../", develop = true } # Development -flwr-datasets = { extras = ["vision"], version = ">=0.0.2,<1.0.0" } -scikit-learn = "^1.1.1" +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 diff --git a/examples/sklearn-logreg-mnist/requirements.txt b/examples/sklearn-logreg-mnist/requirements.txt deleted file mode 100644 index 50da9ace3630..000000000000 --- a/examples/sklearn-logreg-mnist/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr>=1.0, <2.0 -flwr-datasets[vision]>=0.0.2, <1.0.0 -numpy~=1.21.1 -scikit_learn~=1.2.2 diff --git a/examples/sklearn-logreg-mnist/run.sh b/examples/sklearn-logreg-mnist/run.sh deleted file mode 100755 index f770ca05f8f4..000000000000 --- a/examples/sklearn-logreg-mnist/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in $(seq 0 1); do - echo "Starting client $i" - python client.py --partition-id "${i}" & -done - -# This will allow you to use CTRL+C to stop all background processes -trap 'trap - SIGTERM && kill -- -$$' SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/sklearn-logreg-mnist/server.py b/examples/sklearn-logreg-mnist/server.py deleted file mode 100644 index e0af91fabcee..000000000000 --- a/examples/sklearn-logreg-mnist/server.py +++ /dev/null @@ -1,47 +0,0 @@ -import flwr as fl -import utils -from sklearn.metrics import log_loss -from sklearn.linear_model import LogisticRegression -from typing import Dict - -from flwr_datasets import FederatedDataset - - -def fit_round(server_round: int) -> Dict: - """Send round number to client.""" - return {"server_round": server_round} - - -def get_evaluate_fn(model: LogisticRegression): - """Return an evaluation function for server-side evaluation.""" - - # Load test data here to avoid the overhead of doing it in `evaluate` itself - fds = FederatedDataset(dataset="mnist", partitioners={"train": 10}) - dataset = fds.load_split("test").with_format("numpy") - X_test, y_test = dataset["image"].reshape((len(dataset), -1)), dataset["label"] - - # The `evaluate` function will be called after every round - def evaluate(server_round, parameters: fl.common.NDArrays, config): - # Update model with the latest parameters - utils.set_model_params(model, parameters) - loss = log_loss(y_test, model.predict_proba(X_test)) - accuracy = model.score(X_test, y_test) - return loss, {"accuracy": accuracy} - - return evaluate - - -# Start Flower server for five rounds of federated learning -if __name__ == "__main__": - model = LogisticRegression() - utils.set_initial_params(model) - strategy = fl.server.strategy.FedAvg( - min_available_clients=2, - evaluate_fn=get_evaluate_fn(model), - on_fit_config_fn=fit_round, - ) - fl.server.start_server( - server_address="0.0.0.0:8080", - strategy=strategy, - config=fl.server.ServerConfig(num_rounds=5), - ) diff --git a/examples/sklearn-logreg-mnist/sklearnexample/__init__.py b/examples/sklearn-logreg-mnist/sklearnexample/__init__.py new file mode 100644 index 000000000000..e989fe23da02 --- /dev/null +++ b/examples/sklearn-logreg-mnist/sklearnexample/__init__.py @@ -0,0 +1 @@ +"""sklearn_example.""" diff --git a/examples/sklearn-logreg-mnist/sklearnexample/client_app.py b/examples/sklearn-logreg-mnist/sklearnexample/client_app.py new file mode 100644 index 000000000000..ef1dd8618f06 --- /dev/null +++ b/examples/sklearn-logreg-mnist/sklearnexample/client_app.py @@ -0,0 +1,60 @@ +"""sklearnexample: A Flower / scikit-learn app.""" + +import warnings + +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context + +from sklearn.metrics import log_loss +from sklearnexample.task import ( + create_log_reg_and_instantiate_parameters, + get_model_parameters, + load_data, + set_model_params, +) + + +# Define Flower client +class MnistClient(NumPyClient): + def __init__(self, model, X_train, X_test, y_train, y_test): + self.model = model + self.X_train = X_train + self.X_test = X_test + self.y_train = y_train + self.y_test = y_test + + def fit(self, parameters, config): + set_model_params(self.model, parameters) + # Ignore convergence failure due to low local epochs + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.model.fit(self.X_train, self.y_train) + return get_model_parameters(self.model), len(self.X_train), {} + + def evaluate(self, parameters, config): + set_model_params(self.model, parameters) + loss = log_loss(self.y_test, self.model.predict_proba(self.X_test)) + accuracy = self.model.score(self.X_test, self.y_test) + return loss, len(self.X_test), {"accuracy": accuracy} + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + X_train, X_test, y_train, y_test = load_data(partition_id, num_partitions) + + # Read the run config to get settings to configure the Client + penalty = context.run_config["penalty"] + + # Create LogisticRegression Model + model = create_log_reg_and_instantiate_parameters(penalty) + + # Return Client instance + return MnistClient(model, X_train, X_test, y_train, y_test).to_client() + + +# Create ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/sklearn-logreg-mnist/sklearnexample/server_app.py b/examples/sklearn-logreg-mnist/sklearnexample/server_app.py new file mode 100644 index 000000000000..7976cdd08ca9 --- /dev/null +++ b/examples/sklearn-logreg-mnist/sklearnexample/server_app.py @@ -0,0 +1,64 @@ +"""sklearnexample: A Flower / scikit-learn app.""" + +from flwr.common import Context, NDArrays, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg +from flwr_datasets import FederatedDataset + +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import log_loss +from sklearnexample.task import ( + create_log_reg_and_instantiate_parameters, + get_model_parameters, + set_initial_params, + set_model_params, +) + + +def get_evaluate_fn(penalty): + """Return an evaluation function for server-side evaluation.""" + + model = LogisticRegression(penalty=penalty) + set_initial_params(model) + + # Load test data here to avoid the overhead of doing it in `evaluate` itself + fds = FederatedDataset(dataset="mnist", partitioners={"train": 10}) + dataset = fds.load_split("test").with_format("numpy") + X_test, y_test = dataset["image"].reshape((len(dataset), -1)), dataset["label"] + + # The `evaluate` function will be called after every round + def evaluate(server_round, parameters: NDArrays, config): + # Update model with the latest parameters + set_model_params(model, parameters) + loss = log_loss(y_test, model.predict_proba(X_test)) + accuracy = model.score(X_test, y_test) + return loss, {"accuracy": accuracy} + + return evaluate + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components that set the ServerApp behaviour.""" + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + + penalty = context.run_config["penalty"] + model = create_log_reg_and_instantiate_parameters(penalty) + ndarrays = get_model_parameters(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define the strategy + fraction_fit = context.run_config["fraction-fit"] + strategy = FedAvg( + fraction_fit=fraction_fit, + evaluate_fn=get_evaluate_fn(penalty), + initial_parameters=global_model_init, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/sklearn-logreg-mnist/sklearnexample/task.py b/examples/sklearn-logreg-mnist/sklearnexample/task.py new file mode 100644 index 000000000000..38d4203b736c --- /dev/null +++ b/examples/sklearn-logreg-mnist/sklearnexample/task.py @@ -0,0 +1,82 @@ +"""sklearnexample: A Flower / scikit-learn app.""" + +import numpy as np +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from sklearn.linear_model import LogisticRegression + +from flwr.common import NDArrays + +# This information is needed to create a correct scikit-learn model +NUM_UNIQUE_LABELS = 10 # MNIST has 10 classes +NUM_FEATURES = 784 # Number of features in MNIST dataset + + +def get_model_parameters(model: LogisticRegression) -> NDArrays: + """Returns the parameters of a sklearn LogisticRegression model.""" + if model.fit_intercept: + params = [ + model.coef_, + model.intercept_, + ] + else: + params = [ + model.coef_, + ] + return params + + +def set_model_params(model: LogisticRegression, params: NDArrays) -> LogisticRegression: + """Sets the parameters of a sklean LogisticRegression model.""" + model.coef_ = params[0] + if model.fit_intercept: + model.intercept_ = params[1] + return model + + +def set_initial_params(model: LogisticRegression) -> None: + """Sets initial parameters as zeros Required since model params are uninitialized + until model.fit is called. + + But server asks for initial parameters from clients at launch. Refer to + sklearn.linear_model.LogisticRegression documentation for more information. + """ + model.classes_ = np.arange(NUM_UNIQUE_LABELS) + + model.coef_ = np.zeros((NUM_UNIQUE_LABELS, NUM_FEATURES)) + if model.fit_intercept: + model.intercept_ = np.zeros((NUM_UNIQUE_LABELS,)) + + +def create_log_reg_and_instantiate_parameters(penalty): + """Helper function to create a LogisticRegression model.""" + model = LogisticRegression( + penalty=penalty, + max_iter=1, # local epoch + warm_start=True, # prevent refreshing weights when fitting, + ) + # Setting initial parameters, akin to model.compile for keras models + set_initial_params(model) + return model + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + ) + + dataset = fds.load_partition(partition_id, "train").with_format("numpy") + X, y = dataset["image"].reshape((len(dataset), -1)), dataset["label"] + # Split the on edge data: 80% train, 20% test + X_train, X_test = X[: int(0.8 * len(X))], X[int(0.8 * len(X)) :] + y_train, y_test = y[: int(0.8 * len(y))], y[int(0.8 * len(y)) :] + + return X_train, X_test, y_train, y_test diff --git a/examples/sklearn-logreg-mnist/utils.py b/examples/sklearn-logreg-mnist/utils.py deleted file mode 100644 index b279a0d1a4b3..000000000000 --- a/examples/sklearn-logreg-mnist/utils.py +++ /dev/null @@ -1,42 +0,0 @@ -import numpy as np -from sklearn.linear_model import LogisticRegression - -from flwr.common import NDArrays - - -def get_model_parameters(model: LogisticRegression) -> NDArrays: - """Returns the parameters of a sklearn LogisticRegression model.""" - if model.fit_intercept: - params = [ - model.coef_, - model.intercept_, - ] - else: - params = [ - model.coef_, - ] - return params - - -def set_model_params(model: LogisticRegression, params: NDArrays) -> LogisticRegression: - """Sets the parameters of a sklean LogisticRegression model.""" - model.coef_ = params[0] - if model.fit_intercept: - model.intercept_ = params[1] - return model - - -def set_initial_params(model: LogisticRegression): - """Sets initial parameters as zeros Required since model params are uninitialized - until model.fit is called. - - But server asks for initial parameters from clients at launch. Refer to - sklearn.linear_model.LogisticRegression documentation for more information. - """ - n_classes = 10 # MNIST has 10 classes - n_features = 784 # Number of features in dataset - model.classes_ = np.array([i for i in range(10)]) - - model.coef_ = np.zeros((n_classes, n_features)) - if model.fit_intercept: - model.intercept_ = np.zeros((n_classes,)) From 2005fe908cc9ac9ca3f55910b40a31a4eab142c3 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Sun, 11 Aug 2024 00:51:59 +0100 Subject: [PATCH 042/188] fix(framework:skip) Skip certain `grpcio` versions (#3961) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cac01a9ce97a..7d72f8caf32d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" python = "^3.8" # Mandatory dependencies numpy = "^1.21.0" -grpcio = "^1.60.0,!=1.64.2,!=1.65.1" +grpcio = "^1.60.0,!=1.64.2,!=1.65.1,!=1.65.2,!=1.65.4" protobuf = "^4.25.2" cryptography = "^42.0.4" pycryptodome = "^3.18.0" From a6a4acdc96584a0c4055441875dfb3df1792f6d0 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 20:02:55 +0200 Subject: [PATCH 043/188] docs(framework:skip) Fix rST formatting of the MLX and PyTorch tutorials (#3972) Co-authored-by: Taner Topal Co-authored-by: jafermarq --- doc/source/tutorial-quickstart-mlx.rst | 687 +++++++++++---------- doc/source/tutorial-quickstart-pytorch.rst | 635 ++++++++++--------- 2 files changed, 703 insertions(+), 619 deletions(-) diff --git a/doc/source/tutorial-quickstart-mlx.rst b/doc/source/tutorial-quickstart-mlx.rst index 6aa7c8d3aa13..0999bf44d3b7 100644 --- a/doc/source/tutorial-quickstart-mlx.rst +++ b/doc/source/tutorial-quickstart-mlx.rst @@ -1,385 +1,410 @@ .. _quickstart-mlx: +################ + Quickstart MLX +################ -Quickstart MLX -============== +In this federated learning tutorial we will learn how to train simple +MLP on MNIST using Flower and MLX. It is recommended to create a virtual +environment and run everything within a :doc:`virtualenv +`. +Let's use `flwr new` to create a complete Flower+MLX project. It will +generate all the files needed to run, by default with the Simulation +Engine, a federation of 10 nodes using `FedAvg +`_. +The dataset will be partitioned using Flower Dataset's `IidPartitioner +`_. -In this federated learning tutorial we will learn how to train simple MLP on MNIST using Flower and MLX. It is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. +Now that we have a rough idea of what this example is about, let's get +started. First, install Flower in your new environment: -Let's use `flwr new` to create a complete Flower+MLX project. It will generate all the files needed to run, by default with the Simulation Engine, a federation of 10 nodes using `FedAvg `_. The dataset will be partitioned using Flower Dataset's `IidPartitioner `_. +.. code:: shell -Now that we have a rough idea of what this example is about, let's get started. First, install Flower in your new environment: + # In a new Python environment + $ pip install flwr -.. code-block:: shell +Then, run the command below. You will be prompted to select of the +available templates (choose ``MLX``), give a name to your project, and +type in your developer name: - # In a new Python environment - $ pip install flwr +.. code:: shell -Then, run the command below. You will be prompted to select of the available templates (choose :code:`MLX`), give a name to your project, and type in your developer name: + $ flwr new -.. code-block:: shell +After running it you'll notice a new directory with your project name +has been created. It should have the following structure: - $ flwr new +.. code:: shell + + β”œβ”€β”€ + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp + β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp + β”‚ └── task.py # Defines your model, training and data loading + β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs + └── README.md -After running it you'll notice a new directory with your project name has been created. It should have the following structure: +If you haven't yet installed the project and its dependencies, you can +do so by: -.. code-block:: shell +.. code:: shell - - β”œβ”€β”€ - β”‚ β”œβ”€β”€ __init__.py - β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp - β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp - β”‚ └── task.py # Defines your model, training and data loading - β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs - └── README.md - - -If you haven't yet installed the project and its dependencies, you can do so by: - -.. code-block:: shell - - # From the directory where your pyproject.toml is - $ pip install -e . + # From the directory where your pyproject.toml is + $ pip install -e . To run the project do: -.. code-block:: shell +.. code:: shell - # Run with default arguments - $ flwr run . + # Run with default arguments + $ flwr run . With default arguments you will see an output like this one: -.. code-block:: shell - - Loading project configuration... - Success - INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout - INFO : - INFO : [INIT] - INFO : Requesting initial parameters from one random client - WARNING : FAB ID is not provided; the default ClientApp will be loaded. - INFO : Received initial parameters from one random client - INFO : Evaluating initial global parameters - INFO : - INFO : [ROUND 1] - INFO : configure_fit: strategy sampled 10 clients (out of 10) - INFO : aggregate_fit: received 10 results and 0 failures - WARNING : No fit_metrics_aggregation_fn provided - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - WARNING : No evaluate_metrics_aggregation_fn provided - INFO : - INFO : [ROUND 2] - INFO : configure_fit: strategy sampled 10 clients (out of 10) - INFO : aggregate_fit: received 10 results and 0 failures - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - INFO : - INFO : [ROUND 3] - INFO : configure_fit: strategy sampled 10 clients (out of 10) - INFO : aggregate_fit: received 10 results and 0 failures - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - INFO : - INFO : [SUMMARY] - INFO : Run finished 3 round(s) in 8.15s - INFO : History (loss, distributed): - INFO : round 1: 2.243802046775818 - INFO : round 2: 2.101812958717346 - INFO : round 3: 1.7419301986694335 - INFO : - - -You can also override the parameters defined in :code:`[tool.flwr.app.config]` section in the :code:`pyproject.toml` like this: - -.. code-block:: shell - - # Override some arguments - $ flwr run . --run-config num-server-rounds=5,lr=0.05 - - -What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the :code:`ClientApp` and defining the :code:`ServerApp`. - -The Data --------- - -We will use `Flower Datasets `_ to easily download and partition the `MNIST` dataset. -In this example you'll make use of the `IidPartitioner `_ to generate `num_partitions` partitions. -You can choose `other partitioners `_ available in Flower Datasets: - -.. code-block:: python - - partitioner = IidPartitioner(num_partitions=num_partitions) - fds = FederatedDataset( - dataset="ylecun/mnist", - partitioners={"train": partitioner}, - ) - partition = fds.load_partition(partition_id) - partition_splits = partition.train_test_split(test_size=0.2, seed=42) - - partition_splits["train"].set_format("numpy") - partition_splits["test"].set_format("numpy") - - train_partition = partition_splits["train"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", - ) - test_partition = partition_splits["test"].map( - lambda img: { - "img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0 - }, - input_columns="image", - ) - - data = ( - train_partition["img"], - train_partition["label"].astype(np.uint32), - test_partition["img"], - test_partition["label"].astype(np.uint32), - ) - - train_images, train_labels, test_images, test_labels = map(mx.array, data) - - -The Model ---------- - -We define the model as in the `centralized MLX example `_, it's a simple MLP: - -.. code-block:: python - - class MLP(nn.Module): - """A simple MLP.""" - - def __init__( - self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int - ): - super().__init__() - layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] - self.layers = [ - nn.Linear(idim, odim) - for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) - ] - - def __call__(self, x): - for l in self.layers[:-1]: - x = mx.maximum(l(x), 0.0) - return self.layers[-1](x) - -We also define some utility functions to test our model and to iterate over batches. - -.. code-block:: python - - def loss_fn(model, X, y): - return mx.mean(nn.losses.cross_entropy(model(X), y)) - - - def eval_fn(model, X, y): - return mx.mean(mx.argmax(model(X), axis=1) == y) - - - def batch_iterate(batch_size, X, y): - perm = mx.array(np.random.permutation(y.size)) - for s in range(0, y.size, batch_size): - ids = perm[s : s + batch_size] - yield X[ids], y[ids] +.. code:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Requesting initial parameters from one random client + WARNING : FAB ID is not provided; the default ClientApp will be loaded. + INFO : Received initial parameters from one random client + INFO : Evaluating initial global parameters + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + WARNING : No fit_metrics_aggregation_fn provided + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + WARNING : No evaluate_metrics_aggregation_fn provided + INFO : + INFO : [ROUND 2] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [ROUND 3] + INFO : configure_fit: strategy sampled 10 clients (out of 10) + INFO : aggregate_fit: received 10 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 8.15s + INFO : History (loss, distributed): + INFO : round 1: 2.243802046775818 + INFO : round 2: 2.101812958717346 + INFO : round 3: 1.7419301986694335 + INFO : + +You can also override the parameters defined in +``[tool.flwr.app.config]`` section in the ``pyproject.toml`` like this: + +.. code:: shell + + # Override some arguments + $ flwr run . --run-config num-server-rounds=5,lr=0.05 + +What follows is an explanation of each component in the project you just +created: dataset partition, the model, defining the ``ClientApp`` and +defining the ``ServerApp``. + +********** + The Data +********** + +We will use `Flower Datasets `_ to +easily download and partition the `MNIST` dataset. In this example +you'll make use of the `IidPartitioner +`_ +to generate `num_partitions` partitions. You can choose `other +partitioners +`_ +available in Flower Datasets: + +.. code:: python + + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + partition_splits = partition.train_test_split(test_size=0.2, seed=42) + + partition_splits["train"].set_format("numpy") + partition_splits["test"].set_format("numpy") + + train_partition = partition_splits["train"].map( + lambda img: {"img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0}, + input_columns="image", + ) + test_partition = partition_splits["test"].map( + lambda img: {"img": img.reshape(-1, 28 * 28).squeeze().astype(np.float32) / 255.0}, + input_columns="image", + ) + + data = ( + train_partition["img"], + train_partition["label"].astype(np.uint32), + test_partition["img"], + test_partition["label"].astype(np.uint32), + ) + + train_images, train_labels, test_images, test_labels = map(mx.array, data) + +*********** + The Model +*********** + +We define the model as in the `centralized MLX example +`_, it's a +simple MLP: + +.. code:: python + + class MLP(nn.Module): + """A simple MLP.""" + + def __init__( + self, num_layers: int, input_dim: int, hidden_dim: int, output_dim: int + ): + super().__init__() + layer_sizes = [input_dim] + [hidden_dim] * num_layers + [output_dim] + self.layers = [ + nn.Linear(idim, odim) + for idim, odim in zip(layer_sizes[:-1], layer_sizes[1:]) + ] + + def __call__(self, x): + for l in self.layers[:-1]: + x = mx.maximum(l(x), 0.0) + return self.layers[-1](x) + +We also define some utility functions to test our model and to iterate +over batches. + +.. code:: python + + def loss_fn(model, X, y): + return mx.mean(nn.losses.cross_entropy(model(X), y)) + + + def eval_fn(model, X, y): + return mx.mean(mx.argmax(model(X), axis=1) == y) + + def batch_iterate(batch_size, X, y): + perm = mx.array(np.random.permutation(y.size)) + for s in range(0, y.size, batch_size): + ids = perm[s : s + batch_size] + yield X[ids], y[ids] The ClientApp -------------- +============= -The main changes we have to make to use `MLX` with `Flower` will be found in -the :code:`get_params()` and :code:`set_params()` functions. Indeed, MLX doesn't -provide an easy way to convert the model parameters into a list of :code:`np.array` objects -(the format we need for the serialization of the messages to work). +The main changes we have to make to use `MLX` with `Flower` will be +found in the ``get_params()`` and ``set_params()`` functions. Indeed, +MLX doesn't provide an easy way to convert the model parameters into a +list of ``np.array`` objects (the format we need for the serialization +of the messages to work). The way MLX stores its parameters is as follows: -.. code-block:: shell - - { - "layers": [ - {"weight": mlx.core.array, "bias": mlx.core.array}, - {"weight": mlx.core.array, "bias": mlx.core.array}, - ..., - {"weight": mlx.core.array, "bias": mlx.core.array} - ] - } - -Therefore, to get our list of :code:`np.array`s, we need to extract each array and -convert them into a NumPy array: - -.. code-block:: python - - def get_params(model): - layers = model.parameters()["layers"] - return [np.array(val) for layer in layers for _, val in layer.items()] - - -For the :code:`set_params()` function, we perform the reverse operation. We receive -a list of NumPy arrays and want to convert them into MLX parameters. Therefore, we -iterate through pairs of parameters and assign them to the `weight` and `bias` -keys of each layer dict: - -.. code-block:: python - - def set_params(model, parameters): - new_params = {} - new_params["layers"] = [ - {"weight": mx.array(parameters[i]), "bias": mx.array(parameters[i + 1])} - for i in range(0, len(parameters), 2) - ] - model.update(new_params) - - -The rest of the functionality is directly inspired by the centralized case. The :code:`fit()` -method in the client trains the model using the local dataset: - -.. code-block:: python - - def fit(self, parameters, config): - self.set_parameters(parameters) - for _ in range(self.num_epochs): - for X, y in batch_iterate( - self.batch_size, self.train_images, self.train_labels - ): - _, grads = self.loss_and_grad_fn(self.model, X, y) - self.optimizer.update(self.model, grads) - mx.eval(self.model.parameters(), self.optimizer.state) - return self.get_parameters(config={}), len(self.train_images), {} - +.. code:: shell + + { + "layers": [ + {"weight": mlx.core.array, "bias": mlx.core.array}, + {"weight": mlx.core.array, "bias": mlx.core.array}, + ..., + {"weight": mlx.core.array, "bias": mlx.core.array} + ] + } + +Therefore, to get our list of ``np.array`` objects, we need to extract +each array and convert them into a NumPy array: + +.. code:: python + + def get_params(model): + layers = model.parameters()["layers"] + return [np.array(val) for layer in layers for _, val in layer.items()] + +For the ``set_params()`` function, we perform the reverse operation. We +receive a list of NumPy arrays and want to convert them into MLX +parameters. Therefore, we iterate through pairs of parameters and assign +them to the `weight` and `bias` keys of each layer dict: + +.. code:: python + + def set_params(model, parameters): + new_params = {} + new_params["layers"] = [ + {"weight": mx.array(parameters[i]), "bias": mx.array(parameters[i + 1])} + for i in range(0, len(parameters), 2) + ] + model.update(new_params) + +The rest of the functionality is directly inspired by the centralized +case. The ``fit()`` method in the client trains the model using the +local dataset: + +.. code:: python + + def fit(self, parameters, config): + self.set_parameters(parameters) + for _ in range(self.num_epochs): + for X, y in batch_iterate( + self.batch_size, self.train_images, self.train_labels + ): + _, grads = self.loss_and_grad_fn(self.model, X, y) + self.optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), self.optimizer.state) + return self.get_parameters(config={}), len(self.train_images), {} Here, after updating the parameters, we perform the training as in the centralized case, and return the new parameters. -And for the :code:`evaluate()` method of the client: - -.. code-block:: python +And for the ``evaluate()`` method of the client: - def evaluate(self, parameters, config): - self.set_parameters(parameters) - accuracy = eval_fn(self.model, self.test_images, self.test_labels) - loss = loss_fn(self.model, self.test_images, self.test_labels) - return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} +.. code:: python + def evaluate(self, parameters, config): + self.set_parameters(parameters) + accuracy = eval_fn(self.model, self.test_images, self.test_labels) + loss = loss_fn(self.model, self.test_images, self.test_labels) + return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} -We also begin by updating the parameters with the ones sent by the server, and -then we compute the loss and accuracy using the functions defined above. In the -constructor of the :code:`FlowerClient` we instantiate the `MLP` model as well as other -components such as the optimizer. +We also begin by updating the parameters with the ones sent by the +server, and then we compute the loss and accuracy using the functions +defined above. In the constructor of the ``FlowerClient`` we instantiate +the `MLP` model as well as other components such as the optimizer. Putting everything together we have: -.. code-block:: python - - class FlowerClient(NumPyClient): - def __init__( - self, - data, - num_layers, - hidden_dim, - num_classes, - batch_size, - learning_rate, - num_epochs, - ): - self.num_layers = num_layers - self.hidden_dim = hidden_dim - self.num_classes = num_classes - self.batch_size = batch_size - self.learning_rate = learning_rate - self.num_epochs = num_epochs - - self.train_images, self.train_labels, self.test_images, self.test_labels = data - self.model = MLP( - num_layers, self.train_images.shape[-1], hidden_dim, num_classes - ) - self.optimizer = optim.SGD(learning_rate=learning_rate) - self.loss_and_grad_fn = nn.value_and_grad(self.model, loss_fn) - self.num_epochs = num_epochs - self.batch_size = batch_size - - def get_parameters(self, config): - return get_params(self.model) - - def set_parameters(self, parameters): - set_params(self.model, parameters) - - def fit(self, parameters, config): - self.set_parameters(parameters) - for _ in range(self.num_epochs): - for X, y in batch_iterate( - self.batch_size, self.train_images, self.train_labels - ): - _, grads = self.loss_and_grad_fn(self.model, X, y) - self.optimizer.update(self.model, grads) - mx.eval(self.model.parameters(), self.optimizer.state) - return self.get_parameters(config={}), len(self.train_images), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - accuracy = eval_fn(self.model, self.test_images, self.test_labels) - loss = loss_fn(self.model, self.test_images, self.test_labels) - return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} - - -Finally, we can construct a :code:`ClientApp` using the :code:`FlowerClient` defined above by means of a :code:`client_fn()` callback. Note that :code:`context` enables you to get access to hyperparemeters defined in :code:`pyproject.toml` to configure the run. In this tutorial we access, among other hyperparameters, the :code:`local-epochs` setting to control the number of epochs a :code:`ClientApp` will perform when running the :code:`fit()` method. - -.. code-block:: python - - def client_fn(context: Context): - partition_id = context.node_config["partition-id"] - num_partitions = context.node_config["num-partitions"] - data = load_data(partition_id, num_partitions) - - num_layers = context.run_config["num-layers"] - hidden_dim = context.run_config["hidden-dim"] - num_classes = 10 - batch_size = context.run_config["batch-size"] - learning_rate = context.run_config["lr"] - num_epochs = context.run_config["local-epochs"] - - # Return Client instance - return FlowerClient( - data, num_layers, hidden_dim, num_classes, batch_size, learning_rate, num_epochs - ).to_client() - - - # Flower ClientApp - app = ClientApp(client_fn) +.. code:: python + + class FlowerClient(NumPyClient): + def __init__( + self, + data, + num_layers, + hidden_dim, + num_classes, + batch_size, + learning_rate, + num_epochs, + ): + self.num_layers = num_layers + self.hidden_dim = hidden_dim + self.num_classes = num_classes + self.batch_size = batch_size + self.learning_rate = learning_rate + self.num_epochs = num_epochs + + self.train_images, self.train_labels, self.test_images, self.test_labels = data + self.model = MLP( + num_layers, self.train_images.shape[-1], hidden_dim, num_classes + ) + self.optimizer = optim.SGD(learning_rate=learning_rate) + self.loss_and_grad_fn = nn.value_and_grad(self.model, loss_fn) + self.num_epochs = num_epochs + self.batch_size = batch_size + + def get_parameters(self, config): + return get_params(self.model) + + def set_parameters(self, parameters): + set_params(self.model, parameters) + + def fit(self, parameters, config): + self.set_parameters(parameters) + for _ in range(self.num_epochs): + for X, y in batch_iterate( + self.batch_size, self.train_images, self.train_labels + ): + _, grads = self.loss_and_grad_fn(self.model, X, y) + self.optimizer.update(self.model, grads) + mx.eval(self.model.parameters(), self.optimizer.state) + return self.get_parameters(config={}), len(self.train_images), {} + + def evaluate(self, parameters, config): + self.set_parameters(parameters) + accuracy = eval_fn(self.model, self.test_images, self.test_labels) + loss = loss_fn(self.model, self.test_images, self.test_labels) + return loss.item(), len(self.test_images), {"accuracy": accuracy.item()} + +Finally, we can construct a ``ClientApp`` using the ``FlowerClient`` +defined above by means of a ``client_fn()`` callback. Note that +``context`` enables you to get access to hyperparemeters defined in +``pyproject.toml`` to configure the run. In this tutorial we access, +among other hyperparameters, the ``local-epochs`` setting to control the +number of epochs a ``ClientApp`` will perform when running the ``fit()`` +method. + +.. code:: python + + def client_fn(context: Context): + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + data = load_data(partition_id, num_partitions) + + num_layers = context.run_config["num-layers"] + hidden_dim = context.run_config["hidden-dim"] + num_classes = 10 + batch_size = context.run_config["batch-size"] + learning_rate = context.run_config["lr"] + num_epochs = context.run_config["local-epochs"] + + # Return Client instance + return FlowerClient( + data, num_layers, hidden_dim, num_classes, batch_size, learning_rate, num_epochs + ).to_client() + + + # Flower ClientApp + app = ClientApp(client_fn) The ServerApp ------------- -To construct a :code:`ServerApp`, we define a :code:`server_fn()` callback with an identical signature -to that of :code:`client_fn()`, but the return type is `ServerAppComponents `_ as opposed to `Client `_. In this example we use the :code:`FedAvg` strategy. - -.. code-block:: python +To construct a ``ServerApp``, we define a ``server_fn()`` callback with +an identical signature to that of ``client_fn()``, but the return type +is `ServerAppComponents +`_ +as opposed to `Client +`_. +In this example we use the ``FedAvg`` strategy. - def server_fn(context: Context): - # Read from config - num_rounds = context.run_config["num-server-rounds"] +.. code:: python - # Define strategy - strategy = FedAvg() - config = ServerConfig(num_rounds=num_rounds) + def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] - return ServerAppComponents(strategy=strategy, config=config) + # Define strategy + strategy = FedAvg() + config = ServerConfig(num_rounds=num_rounds) + return ServerAppComponents(strategy=strategy, config=config) - # Create ServerApp - app = ServerApp(server_fn=server_fn) + # Create ServerApp + app = ServerApp(server_fn=server_fn) -Congratulations! -You've successfully built and run your first federated learning system. +Congratulations! You've successfully built and run your first federated +learning system. .. note:: - Check the `source code `_ of the extended version of this tutorial in :code:`examples/quickstart-mlx` in the Flower GitHub repository. + Check the `source code + `_ + of the extended version of this tutorial in + ``examples/quickstart-mlx`` in the Flower GitHub repository. diff --git a/doc/source/tutorial-quickstart-pytorch.rst b/doc/source/tutorial-quickstart-pytorch.rst index d768ae7a2fd7..4515e8d0eeb5 100644 --- a/doc/source/tutorial-quickstart-pytorch.rst +++ b/doc/source/tutorial-quickstart-pytorch.rst @@ -1,325 +1,384 @@ .. _quickstart-pytorch: +#################### + Quickstart PyTorch +#################### -Quickstart PyTorch -================== +In this federated learning tutorial we will learn how to train a +Convolutional Neural Network on CIFAR-10 using Flower and PyTorch. It is +recommended to create a virtual environment and run everything within a +:doc:`virtualenv `. -In this federated learning tutorial we will learn how to train a Convolutional Neural Network on CIFAR-10 using Flower and PyTorch. It is recommended to create a virtual environment and run everything within a :doc:`virtualenv `. +Let's use `flwr new` to create a complete Flower+PyTorch project. It +will generate all the files needed to run, by default with the Flower +Simulation Engine, a federation of 10 nodes using `FedAvg +`_. +The dataset will be partitioned using Flower Dataset's `IidPartitioner +`_. -Let's use `flwr new` to create a complete Flower+PyTorch project. It will generate all the files needed to run, by default with the Flower Simulation Engine, a federation of 10 nodes using `FedAvg `_. The dataset will be partitioned using Flower Dataset's `IidPartitioner `_. +Now that we have a rough idea of what this example is about, let's get +started. First, install Flower in your new environment: -Now that we have a rough idea of what this example is about, let's get started. First, install Flower in your new environment: +.. code:: shell -.. code-block:: shell + # In a new Python environment + $ pip install flwr - # In a new Python environment - $ pip install flwr +Then, run the command below. You will be prompted to select one of the +available templates (choose ``PyTorch``), give a name to your project, +and type in your developer name: -Then, run the command below. You will be prompted to select one of the available templates (choose :code:`PyTorch`), give a name to your project, and type in your developer name: +.. code:: shell -.. code-block:: shell + $ flwr new - $ flwr new +After running it you'll notice a new directory with your project name +has been created. It should have the following structure: -After running it you'll notice a new directory with your project name has been created. It should have the following structure: +.. code:: shell -.. code-block:: shell + + β”œβ”€β”€ + β”‚ β”œβ”€β”€ __init__.py + β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp + β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp + β”‚ └── task.py # Defines your model, training and data loading + β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs + └── README.md - - β”œβ”€β”€ - β”‚ β”œβ”€β”€ __init__.py - β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp - β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp - β”‚ └── task.py # Defines your model, training and data loading - β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs - └── README.md +If you haven't yet installed the project and its dependencies, you can +do so by: +.. code:: shell -If you haven't yet installed the project and its dependencies, you can do so by: - -.. code-block:: shell - - # From the directory where your pyproject.toml is - $ pip install -e . + # From the directory where your pyproject.toml is + $ pip install -e . To run the project, do: -.. code-block:: shell +.. code:: shell - # Run with default arguments - $ flwr run . + # Run with default arguments + $ flwr run . With default arguments you will see an output like this one: -.. code-block:: shell - - Loading project configuration... - Success - WARNING : FAB ID is not provided; the default ClientApp will be loaded. - INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout - INFO : - INFO : [INIT] - INFO : Using initial global parameters provided by strategy - INFO : Evaluating initial global parameters - INFO : - INFO : [ROUND 1] - INFO : configure_fit: strategy sampled 5 clients (out of 10) - INFO : aggregate_fit: received 5 results and 0 failures - WARNING : No fit_metrics_aggregation_fn provided - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - WARNING : No evaluate_metrics_aggregation_fn provided - INFO : - INFO : [ROUND 2] - INFO : configure_fit: strategy sampled 5 clients (out of 10) - INFO : aggregate_fit: received 5 results and 0 failures - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - INFO : - INFO : [ROUND 3] - INFO : configure_fit: strategy sampled 5 clients (out of 10) - INFO : aggregate_fit: received 5 results and 0 failures - INFO : configure_evaluate: strategy sampled 10 clients (out of 10) - INFO : aggregate_evaluate: received 10 results and 0 failures - INFO : - INFO : [SUMMARY] - INFO : Run finished 3 round(s) in 21.35s - INFO : History (loss, distributed): - INFO : round 1: 2.2978184528648855 - INFO : round 2: 2.173852103948593 - INFO : round 3: 2.039920600131154 - INFO : - -You can also override the parameters defined in the :code:`[tool.flwr.app.config]` section in :code:`pyproject.toml` like this: - -.. code-block:: shell - - # Override some arguments - $ flwr run . --run-config num-server-rounds=5,local-epochs=3 - - -What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the :code:`ClientApp` and defining the :code:`ServerApp`. - - -The Data --------- - -This tutorial uses `Flower Datasets `_ to easily download and partition the `CIFAR-10` dataset. -In this example you'll make use of the `IidPartitioner `_ to generate `num_partitions` partitions. -You can choose `other partitioners `_ available in Flower Datasets. Each :code:`ClientApp` will call this function to create dataloaders with the data that correspond to their data partition. - - -.. code-block:: python - - partitioner = IidPartitioner(num_partitions=num_partitions) - fds = FederatedDataset( - dataset="uoft-cs/cifar10", - partitioners={"train": partitioner}, - ) - partition = fds.load_partition(partition_id) - # Divide data on each node: 80% train, 20% test - partition_train_test = partition.train_test_split(test_size=0.2, seed=42) - pytorch_transforms = Compose( - [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - def apply_transforms(batch): - """Apply transforms to the partition from FederatedDataset.""" - batch["img"] = [pytorch_transforms(img) for img in batch["img"]] - return batch - - partition_train_test = partition_train_test.with_transform(apply_transforms) - trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) - testloader = DataLoader(partition_train_test["test"], batch_size=32) - -The Model ---------- - -We defined a simple Convolutional Neural Network (CNN), but feel free to replace it with a more sophisticated model if you'd like: - -.. code-block:: python - - class Net(nn.Module): - """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" - - def __init__(self): - super(Net, self).__init__() - self.conv1 = nn.Conv2d(3, 6, 5) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, 5) - self.fc1 = nn.Linear(16 * 5 * 5, 120) - self.fc2 = nn.Linear(120, 84) - self.fc3 = nn.Linear(84, 10) - - def forward(self, x): - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = x.view(-1, 16 * 5 * 5) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - return self.fc3(x) - -In addition to defining the model architecture, we also include two utility functions to perform both training (i.e. :code:`train()`) and evaluation (i.e. :code:`test()`) using the above model. These functions should look fairly familiar if you have some prior experience with PyTorch. Note these functions do not have anything specific to Flower. That being said, the training function will normally be called, as we'll see later, from a Flower client passing its own data. In summary, your clients can use standard training/testing functions to perform local training or evaluation: - -.. code-block:: python - - def train(net, trainloader, epochs, device): - """Train the model on the training set.""" - net.to(device) # move model to GPU if available - criterion = torch.nn.CrossEntropyLoss().to(device) - optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9) - net.train() - running_loss = 0.0 - for _ in range(epochs): - for batch in trainloader: - images = batch["img"] - labels = batch["label"] - optimizer.zero_grad() - loss = criterion(net(images.to(device)), labels.to(device)) - loss.backward() - optimizer.step() - running_loss += loss.item() - - avg_trainloss = running_loss / len(trainloader) - return avg_trainloss - - - def test(net, testloader, device): - """Validate the model on the test set.""" - net.to(device) - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - with torch.no_grad(): - for batch in testloader: - images = batch["img"].to(device) - labels = batch["label"].to(device) - outputs = net(images) - loss += criterion(outputs, labels).item() - correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() - accuracy = correct / len(testloader.dataset) - return loss, accuracy - - -The ClientApp -------------- - -The main changes we have to make to use `PyTorch` with `Flower` will be found in -the :code:`get_weights()` and :code:`set_weights()` functions. In :code:`get_weights()` PyTorch model parameters are extracted and represented as a list of NumPy arrays. The :code:`set_weights()` function that's the oposite: given a list of NumPy arrays it applies them to an existing PyTorch model. Doing this in fairly easy in PyTorch. +.. code:: shell + + Loading project configuration... + Success + WARNING : FAB ID is not provided; the default ClientApp will be loaded. + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Evaluating initial global parameters + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + WARNING : No fit_metrics_aggregation_fn provided + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + WARNING : No evaluate_metrics_aggregation_fn provided + INFO : + INFO : [ROUND 2] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [ROUND 3] + INFO : configure_fit: strategy sampled 5 clients (out of 10) + INFO : aggregate_fit: received 5 results and 0 failures + INFO : configure_evaluate: strategy sampled 10 clients (out of 10) + INFO : aggregate_evaluate: received 10 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 21.35s + INFO : History (loss, distributed): + INFO : round 1: 2.2978184528648855 + INFO : round 2: 2.173852103948593 + INFO : round 3: 2.039920600131154 + INFO : + +You can also override the parameters defined in the +``[tool.flwr.app.config]`` section in ``pyproject.toml`` like this: + +.. code:: shell + + # Override some arguments + $ flwr run . --run-config num-server-rounds=5,local-epochs=3 + +What follows is an explanation of each component in the project you just +created: dataset partition, the model, defining the ``ClientApp`` and +defining the ``ServerApp``. + +********** + The Data +********** + +This tutorial uses `Flower Datasets `_ +to easily download and partition the `CIFAR-10` dataset. In this example +you'll make use of the `IidPartitioner +`_ +to generate `num_partitions` partitions. You can choose `other +partitioners +`_ +available in Flower Datasets. Each ``ClientApp`` will call this function +to create dataloaders with the data that correspond to their data +partition. + +.. code:: python + + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose([ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) + + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + +*********** + The Model +*********** + +We defined a simple Convolutional Neural Network (CNN), but feel free to +replace it with a more sophisticated model if you'd like: + +.. code:: python + + class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +In addition to defining the model architecture, we also include two +utility functions to perform both training (i.e. ``train()``) and +evaluation (i.e. ``test()``) using the above model. These functions +should look fairly familiar if you have some prior experience with +PyTorch. Note these functions do not have anything specific to Flower. +That being said, the training function will normally be called, as we'll +see later, from a Flower client passing its own data. In summary, your +clients can use standard training/testing functions to perform local +training or evaluation: + +.. code:: python + + def train(net, trainloader, epochs, device): + """Train the model on the training set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss().to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9) + net.train() + running_loss = 0.0 + for _ in range(epochs): + for batch in trainloader: + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + loss = criterion(net(images.to(device)), labels.to(device)) + loss.backward() + optimizer.step() + running_loss += loss.item() + + avg_trainloss = running_loss / len(trainloader) + return avg_trainloss + + + def test(net, testloader, device): + """Validate the model on the test set.""" + net.to(device) + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + return loss, accuracy + +*************** + The ClientApp +*************** + +The main changes we have to make to use `PyTorch` with `Flower` will be +found in the ``get_weights()`` and ``set_weights()`` functions. In +``get_weights()`` PyTorch model parameters are extracted and represented +as a list of NumPy arrays. The ``set_weights()`` function that's the +oposite: given a list of NumPy arrays it applies them to an existing +PyTorch model. Doing this in fairly easy in PyTorch. .. note:: - The specific implementation of :code:`get_weights()` and :code:`set_weights()` depends on the type of models you use. The ones shown below work for a wide range of PyTorch models but you might need to adjust them if you have more exotic model architectures. - - -.. code-block:: python - - def get_weights(net): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - def set_weights(net, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) - - -The rest of the functionality is directly inspired by the centralized case. The :code:`fit()` method in the client trains the model using the local dataset. -Similarly, the :code:`evaluate()` method is used to evaluate the model received on a held-out validation set that the client might have: - - -.. code-block:: python - - class FlowerClient(NumPyClient): - def __init__(self, net, trainloader, valloader, local_epochs): - self.net = net - self.trainloader = trainloader - self.valloader = valloader - self.local_epochs = local_epochs - self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - self.net.to(device) - - def fit(self, parameters, config): - set_weights(self.net, parameters) - results = train( - self.net, - self.trainloader, - self.valloader, - self.local_epochs, - self.device, - ) - return get_weights(self.net), len(self.trainloader.dataset), results - - def evaluate(self, parameters, config): - set_weights(self.net, parameters) - loss, accuracy = test(self.net, self.valloader, self.device) - return loss, len(self.valloader.dataset), {"accuracy": accuracy} - -Finally, we can construct a :code:`ClientApp` using the :code:`FlowerClient` defined above by means of a :code:`client_fn()` callback. Note that the `context` enables you to get access to hyperparemeters defined in your :code:`pyproject.toml` to configure the run. In this tutorial we access the `local-epochs` setting to control the number of epochs a :code:`ClientApp` will perform when running the :code:`fit()` method. You could define additioinal hyperparameters in :code:`pyproject.toml` and access them here. - -.. code-block:: python - - def client_fn(context: Context): - # Load model and data - net = Net() - partition_id = context.node_config["partition-id"] - num_partitions = context.node_config["num-partitions"] - trainloader, valloader = load_data(partition_id, num_partitions) - local_epochs = context.run_config["local-epochs"] - # Return Client instance - return FlowerClient(net, trainloader, valloader, local_epochs).to_client() - - - # Flower ClientApp - app = ClientApp(client_fn) - - -The ServerApp -------------- - -To construct a :code:`ServerApp` we define a :code:`server_fn()` callback with an identical signature -to that of :code:`client_fn()` but the return type is `ServerAppComponents `_ as opposed to a `Client `_. In this example we use the `FedAvg`. To it we pass a randomly initialized model that will server as the global model to federated. Note that the value of :code:`fraction_fit` is read from the run config. You can find the default value defined in the :code:`pyproject.toml`. - -.. code-block:: python - - def server_fn(context: Context): - # Read from config - num_rounds = context.run_config["num-server-rounds"] - fraction_fit = context.run_config["fraction-fit"] - - # Initialize model parameters - ndarrays = get_weights(Net()) - parameters = ndarrays_to_parameters(ndarrays) - - # Define strategy - strategy = FedAvg( - fraction_fit=fraction_fit, - fraction_evaluate=1.0, - min_available_clients=2, - initial_parameters=parameters, - ) - config = ServerConfig(num_rounds=num_rounds) - - return ServerAppComponents(strategy=strategy, config=config) - - # Create ServerApp - app = ServerApp(server_fn=server_fn) - - -Congratulations! -You've successfully built and run your first federated learning system. + The specific implementation of ``get_weights()`` and + ``set_weights()`` depends on the type of models you use. The ones + shown below work for a wide range of PyTorch models but you might + need to adjust them if you have more exotic model architectures. + +.. code:: python + + def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + + def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) + +The rest of the functionality is directly inspired by the centralized +case. The ``fit()`` method in the client trains the model using the +local dataset. Similarly, the ``evaluate()`` method is used to evaluate +the model received on a held-out validation set that the client might +have: + +.. code:: python + + class FlowerClient(NumPyClient): + def __init__(self, net, trainloader, valloader, local_epochs): + self.net = net + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.net.to(device) + + def fit(self, parameters, config): + set_weights(self.net, parameters) + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.device, + ) + return get_weights(self.net), len(self.trainloader.dataset), results + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} + +Finally, we can construct a ``ClientApp`` using the ``FlowerClient`` +defined above by means of a ``client_fn()`` callback. Note that the +`context` enables you to get access to hyperparemeters defined in your +``pyproject.toml`` to configure the run. In this tutorial we access the +`local-epochs` setting to control the number of epochs a ``ClientApp`` +will perform when running the ``fit()`` method. You could define +additioinal hyperparameters in ``pyproject.toml`` and access them here. + +.. code:: python + + def client_fn(context: Context): + # Load model and data + net = Net() + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + trainloader, valloader = load_data(partition_id, num_partitions) + local_epochs = context.run_config["local-epochs"] + + # Return Client instance + return FlowerClient(net, trainloader, valloader, local_epochs).to_client() + + + # Flower ClientApp + app = ClientApp(client_fn) + +*************** + The ServerApp +*************** + +To construct a ``ServerApp`` we define a ``server_fn()`` callback with +an identical signature to that of ``client_fn()`` but the return type is +`ServerAppComponents +`_ +as opposed to a `Client +`_. +In this example we use the `FedAvg`. To it we pass a randomly +initialized model that will server as the global model to federated. +Note that the value of ``fraction_fit`` is read from the run config. You +can find the default value defined in the ``pyproject.toml``. + +.. code:: python + + def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define strategy + strategy = FedAvg( + fraction_fit=fraction_fit, + fraction_evaluate=1.0, + min_available_clients=2, + initial_parameters=parameters, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + + # Create ServerApp + app = ServerApp(server_fn=server_fn) + +Congratulations! You've successfully built and run your first federated +learning system. .. note:: - Check the `source code `_ of the extended version of this tutorial in :code:`examples/quickstart-pytorch` in the Flower GitHub repository. + Check the `source code + `_ + of the extended version of this tutorial in + ``examples/quickstart-pytorch`` in the Flower GitHub repository. -Video tutorial --------------- +**************** + Video tutorial +**************** .. note:: - The video shown below shows how to setup a PyTorch + Flower project using our previously recommended APIs. A new video tutorial will be released that shows the new APIs (as the content above does) + + The video shown below shows how to setup a PyTorch + Flower project + using our previously recommended APIs. A new video tutorial will be + released that shows the new APIs (as the content above does) .. meta:: :description: Check out this Federated Learning quickstart tutorial for using Flower with PyTorch to train a CNN model on MNIST. -.. youtube:: jOmmuzMIQ4c +.. youtube:: jOmmuzMIQ4c :width: 100% - From f8298e5e535cb080043913ecf987bf502bd573af Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 20:08:35 +0200 Subject: [PATCH 044/188] fix(framework:skip) Remove FAB file once sent to SuperExec (#3851) --- src/py/flwr/cli/run/run.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index 34fbd3a73c7f..c5a096293f01 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -162,10 +162,10 @@ def on_channel_state_change(channel_connectivity: str) -> None: channel.subscribe(on_channel_state_change) stub = ExecStub(channel) - fab_path = build(app) + fab_path = Path(build(app)) req = StartRunRequest( - fab_file=Path(fab_path).read_bytes(), + fab_file=fab_path.read_bytes(), override_config=user_config_to_proto( parse_config_args(config_overrides, separator=",") ), @@ -174,6 +174,9 @@ def on_channel_state_change(channel_connectivity: str) -> None: ), ) res = stub.StartRun(req) + + # Delete FAB file once it has been sent to the SuperExec + fab_path.unlink() typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN) From c8120f2669fef0f2e6815ab1e957e5366d06d19d Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 20:25:22 +0200 Subject: [PATCH 045/188] ci(*:skip) Format imports in `dev`, `doc`, and `examples` (#3991) --- dev/build-docker-image-matrix.py | 2 +- dev/build-example-docs.py | 2 +- dev/check_pr_title.py | 1 - doc/source/conf.py | 1 + examples/advanced-pytorch/client.py | 12 ++++++---- examples/advanced-pytorch/server.py | 12 ++++------ examples/advanced-pytorch/utils.py | 7 +++--- examples/advanced-tensorflow/client.py | 4 +--- examples/advanced-tensorflow/server.py | 3 +-- .../tflite_convertor/convert_to_tflite.py | 5 ++-- .../tfltransfer/bases/mobilenetv2_base.py | 4 +--- .../tfltransfer/bases/quantizable_base.py | 4 +--- .../tfltransfer/bases/saved_model_base.py | 4 +--- .../tfltransfer/heads/keras_model_head.py | 4 +--- .../heads/logits_saved_model_head.py | 4 +--- .../heads/softmax_classifier_head.py | 4 +--- .../tfltransfer/model_correctness_test.py | 9 ++------ .../tfltransfer/optimizers/adam.py | 4 +--- .../tfltransfer/optimizers/sgd.py | 4 +--- .../tfltransfer/tflite_transfer_convert.py | 9 ++------ .../tfltransfer/tflite_transfer_converter.py | 4 +--- .../tflite_transfer_converter_test.py | 9 ++------ .../tflite_convertor/tfltransfer/utils.py | 4 +--- examples/app-pytorch/client.py | 11 +-------- examples/app-pytorch/client_low_level.py | 2 +- examples/app-pytorch/server.py | 2 +- examples/app-pytorch/server_custom.py | 12 +++++----- examples/app-pytorch/server_low_level.py | 7 +++--- examples/app-pytorch/server_workflow.py | 4 ++-- examples/app-pytorch/task.py | 1 - .../custommetrics_example/client_app.py | 4 ++-- .../custommetrics_example/server_app.py | 4 ++-- examples/custom-mods/client.py | 14 +++-------- examples/custom-mods/task.py | 1 - examples/embedded-devices/client_pytorch.py | 5 ++-- examples/embedded-devices/client_tf.py | 5 ++-- examples/embedded-devices/server.py | 1 - .../federated-kaplan-meier-fitter/server.py | 10 ++++---- examples/fl-dp-sa/fl_dp_sa/client.py | 1 - examples/fl-dp-sa/fl_dp_sa/server.py | 7 ++---- examples/fl-dp-sa/fl_dp_sa/task.py | 3 +-- examples/fl-tabular/client.py | 3 ++- examples/fl-tabular/server.py | 1 + examples/fl-tabular/task.py | 11 +++++---- examples/flower-authentication/client.py | 14 +++-------- examples/flower-authentication/server.py | 2 +- examples/flower-authentication/task.py | 1 - .../secaggexample/server_app.py | 7 +++--- .../secaggexample/workflow_with_log.py | 4 ++-- .../Part-I/client.py | 6 ++--- .../Part-I/dataset.py | 4 ++-- .../Part-I/main.py | 7 +++--- .../Part-I/server.py | 4 +--- .../Part-II/client.py | 10 ++++---- .../Part-II/dataset.py | 4 ++-- .../Part-II/main.py | 10 ++++---- .../Part-II/model.py | 1 - .../Part-II/server.py | 6 ++--- .../Part-II/toy.py | 3 +-- examples/flower-via-docker-compose/client.py | 7 +++--- .../helpers/generate_docker_compose.py | 2 +- .../helpers/load_data.py | 3 ++- examples/flower-via-docker-compose/server.py | 6 +++-- .../strategy/strategy.py | 8 ++++--- examples/ios/server.py | 3 ++- examples/llm-flowertune/app.py | 7 +++--- examples/llm-flowertune/client.py | 6 ++--- examples/llm-flowertune/main.py | 10 ++++---- examples/llm-flowertune/models.py | 9 ++++---- examples/llm-flowertune/test.py | 4 ++-- examples/opacus/client.py | 7 +++--- examples/opacus/server.py | 2 +- .../client.py | 4 ++-- .../cifar.py | 5 ++-- .../client.py | 5 ++-- examples/quickstart-cpp/fedavg_cpp.py | 10 ++------ .../fastai_example/client_app.py | 3 +-- examples/quickstart-huggingface/client.py | 10 ++++---- examples/quickstart-huggingface/server.py | 1 - examples/quickstart-jax/client.py | 6 ++--- examples/quickstart-jax/jax_training.py | 5 ++-- examples/quickstart-mlcube/client.py | 2 ++ examples/quickstart-mlcube/mlcube_utils.py | 7 +++--- .../quickstart-mlx/mlxexample/server_app.py | 1 - examples/quickstart-monai/client.py | 6 ++--- examples/quickstart-pandas/client.py | 5 +--- examples/quickstart-pandas/server.py | 3 +-- .../pytorchlightning_example/server_app.py | 1 - .../pytorchexample/client_app.py | 11 ++------- .../pytorchexample/server_app.py | 3 ++- .../quickstart-pytorch/pytorchexample/task.py | 4 ++-- .../sklearnexample/client_app.py | 8 +++---- .../sklearnexample/server_app.py | 6 ++--- .../sklearnexample/task.py | 3 +-- examples/quickstart-tabnet/client.py | 3 ++- examples/quickstart-tabnet/server.py | 1 - .../tfexample/client_app.py | 3 +-- .../tfexample/server_app.py | 4 ++-- .../quickstart-tensorflow/tfexample/task.py | 3 +-- examples/simulation-pytorch/sim.py | 14 +++++------ examples/simulation-pytorch/utils.py | 3 +-- examples/simulation-tensorflow/sim.py | 8 +++---- .../sklearnexample/client_app.py | 2 +- .../sklearnexample/server_app.py | 2 +- .../sklearnexample/task.py | 3 +-- examples/tensorflow-privacy/client.py | 6 ++--- examples/tensorflow-privacy/server.py | 2 +- examples/vertical-fl/plot.py | 2 +- examples/vertical-fl/simulation.py | 6 +++-- examples/vertical-fl/task.py | 2 +- examples/vit-finetune/client.py | 4 ++-- examples/vit-finetune/dataset.py | 7 +++--- examples/vit-finetune/main.py | 2 +- examples/vit-finetune/model.py | 2 +- examples/vit-finetune/server.py | 2 +- .../centralised.py | 16 ++++++------- .../whisper-federated-finetuning/client.py | 13 ++++++----- .../whisper-federated-finetuning/server.py | 7 +++--- examples/whisper-federated-finetuning/sim.py | 3 +-- .../whisper-federated-finetuning/utils.py | 12 ++++------ examples/xgboost-comprehensive/client.py | 9 ++++---- .../xgboost-comprehensive/client_utils.py | 4 ++-- examples/xgboost-comprehensive/dataset.py | 5 ++-- examples/xgboost-comprehensive/server.py | 11 ++++----- .../xgboost-comprehensive/server_utils.py | 6 +++-- examples/xgboost-comprehensive/sim.py | 23 ++++++++----------- examples/xgboost-comprehensive/utils.py | 1 - examples/xgboost-quickstart/client.py | 11 ++++----- examples/xgboost-quickstart/server.py | 2 +- 129 files changed, 287 insertions(+), 408 deletions(-) diff --git a/dev/build-docker-image-matrix.py b/dev/build-docker-image-matrix.py index b7c4d2daaefd..f10ae03245d0 100644 --- a/dev/build-docker-image-matrix.py +++ b/dev/build-docker-image-matrix.py @@ -3,9 +3,9 @@ """ import argparse +import json from dataclasses import asdict, dataclass from enum import Enum -import json from typing import Any, Callable, Dict, List, Optional diff --git a/dev/build-example-docs.py b/dev/build-example-docs.py index 367994708bf9..772a26272fd7 100644 --- a/dev/build-example-docs.py +++ b/dev/build-example-docs.py @@ -15,8 +15,8 @@ """Build the Flower Example docs.""" import os -import shutil import re +import shutil import subprocess from pathlib import Path diff --git a/dev/check_pr_title.py b/dev/check_pr_title.py index 33b7a4664e9f..7dbd17394148 100644 --- a/dev/check_pr_title.py +++ b/dev/check_pr_title.py @@ -19,7 +19,6 @@ import sys import tomllib - if __name__ == "__main__": pr_title = sys.argv[1] diff --git a/doc/source/conf.py b/doc/source/conf.py index 998c50b1dd7b..27cae3f92e1f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -17,6 +17,7 @@ import datetime import os import sys + from git import Repo from sphinx.application import ConfigError diff --git a/examples/advanced-pytorch/client.py b/examples/advanced-pytorch/client.py index 7c1420a2cecd..1b93d45d950e 100644 --- a/examples/advanced-pytorch/client.py +++ b/examples/advanced-pytorch/client.py @@ -1,11 +1,13 @@ -import utils -from torch.utils.data import DataLoader -import torch -import flwr as fl import argparse -from collections import OrderedDict import warnings +from collections import OrderedDict + import datasets +import flwr as fl +import torch +from torch.utils.data import DataLoader + +import utils warnings.filterwarnings("ignore") diff --git a/examples/advanced-pytorch/server.py b/examples/advanced-pytorch/server.py index 489694ab1ea1..6b69512fb3b7 100644 --- a/examples/advanced-pytorch/server.py +++ b/examples/advanced-pytorch/server.py @@ -1,17 +1,15 @@ -from typing import Dict, Optional, Tuple -from collections import OrderedDict import argparse -from torch.utils.data import DataLoader +import warnings +from collections import OrderedDict +from typing import Dict, Optional, Tuple import flwr as fl import torch +from flwr_datasets import FederatedDataset +from torch.utils.data import DataLoader import utils -import warnings - -from flwr_datasets import FederatedDataset - warnings.filterwarnings("ignore") diff --git a/examples/advanced-pytorch/utils.py b/examples/advanced-pytorch/utils.py index c47b4fa38593..d2b3955c9fde 100644 --- a/examples/advanced-pytorch/utils.py +++ b/examples/advanced-pytorch/utils.py @@ -1,10 +1,9 @@ -import torch -from torchvision.transforms import Compose, ToTensor, Normalize, Resize, CenterCrop -from torchvision.models import efficientnet_b0, AlexNet import warnings +import torch from flwr_datasets import FederatedDataset - +from torchvision.models import AlexNet, efficientnet_b0 +from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToTensor warnings.filterwarnings("ignore") diff --git a/examples/advanced-tensorflow/client.py b/examples/advanced-tensorflow/client.py index b658a1f9ea04..b6a485b7ba4c 100644 --- a/examples/advanced-tensorflow/client.py +++ b/examples/advanced-tensorflow/client.py @@ -2,10 +2,8 @@ import os from pathlib import Path -import tensorflow as tf - import flwr as fl - +import tensorflow as tf from flwr_datasets import FederatedDataset # Make TensorFlow logs less verbose diff --git a/examples/advanced-tensorflow/server.py b/examples/advanced-tensorflow/server.py index e159a096dc83..8febdd57614d 100644 --- a/examples/advanced-tensorflow/server.py +++ b/examples/advanced-tensorflow/server.py @@ -1,9 +1,8 @@ -from typing import Dict, Optional, Tuple from pathlib import Path +from typing import Dict, Optional, Tuple import flwr as fl import tensorflow as tf - from flwr_datasets import FederatedDataset diff --git a/examples/android/tflite_convertor/convert_to_tflite.py b/examples/android/tflite_convertor/convert_to_tflite.py index 79259a6cdf87..c4b7d2abf48a 100644 --- a/examples/android/tflite_convertor/convert_to_tflite.py +++ b/examples/android/tflite_convertor/convert_to_tflite.py @@ -1,9 +1,8 @@ import tensorflow as tf from tensorflow.keras import layers from tensorflow.keras.regularizers import l2 -from tfltransfer import bases -from tfltransfer import heads -from tfltransfer import optimizers + +from tfltransfer import bases, heads, optimizers from tfltransfer.tflite_transfer_converter import TFLiteTransferConverter # Define the base model. diff --git a/examples/android/tflite_convertor/tfltransfer/bases/mobilenetv2_base.py b/examples/android/tflite_convertor/tfltransfer/bases/mobilenetv2_base.py index 9e7823cd1030..083778d883fc 100644 --- a/examples/android/tflite_convertor/tfltransfer/bases/mobilenetv2_base.py +++ b/examples/android/tflite_convertor/tfltransfer/bases/mobilenetv2_base.py @@ -14,9 +14,7 @@ """Base model configuration for MobileNetV2.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import tensorflow as tf diff --git a/examples/android/tflite_convertor/tfltransfer/bases/quantizable_base.py b/examples/android/tflite_convertor/tfltransfer/bases/quantizable_base.py index bbdf36a08ef8..0a59120025f6 100644 --- a/examples/android/tflite_convertor/tfltransfer/bases/quantizable_base.py +++ b/examples/android/tflite_convertor/tfltransfer/bases/quantizable_base.py @@ -14,9 +14,7 @@ """Base model abstract base class that handles quantization.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import abc diff --git a/examples/android/tflite_convertor/tfltransfer/bases/saved_model_base.py b/examples/android/tflite_convertor/tfltransfer/bases/saved_model_base.py index d268e54729eb..90678c91e348 100644 --- a/examples/android/tflite_convertor/tfltransfer/bases/saved_model_base.py +++ b/examples/android/tflite_convertor/tfltransfer/bases/saved_model_base.py @@ -14,9 +14,7 @@ """Base model configuration that reads a specified SavedModel.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import tensorflow as tf diff --git a/examples/android/tflite_convertor/tfltransfer/heads/keras_model_head.py b/examples/android/tflite_convertor/tfltransfer/heads/keras_model_head.py index 2b17c6f66f81..5fc13043f78b 100644 --- a/examples/android/tflite_convertor/tfltransfer/heads/keras_model_head.py +++ b/examples/android/tflite_convertor/tfltransfer/heads/keras_model_head.py @@ -14,9 +14,7 @@ """Head model configuration for Keras models.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import os import shutil diff --git a/examples/android/tflite_convertor/tfltransfer/heads/logits_saved_model_head.py b/examples/android/tflite_convertor/tfltransfer/heads/logits_saved_model_head.py index ec0786b9298a..90f2183d2102 100644 --- a/examples/android/tflite_convertor/tfltransfer/heads/logits_saved_model_head.py +++ b/examples/android/tflite_convertor/tfltransfer/heads/logits_saved_model_head.py @@ -14,9 +14,7 @@ """Head model configuration for classifier SavedModels.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import os import shutil diff --git a/examples/android/tflite_convertor/tfltransfer/heads/softmax_classifier_head.py b/examples/android/tflite_convertor/tfltransfer/heads/softmax_classifier_head.py index af869a90b7d3..46e604ff55f5 100644 --- a/examples/android/tflite_convertor/tfltransfer/heads/softmax_classifier_head.py +++ b/examples/android/tflite_convertor/tfltransfer/heads/softmax_classifier_head.py @@ -14,9 +14,7 @@ """Head model configuration for simple softmax classifiers.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import numpy as np import tensorflow as tf diff --git a/examples/android/tflite_convertor/tfltransfer/model_correctness_test.py b/examples/android/tflite_convertor/tfltransfer/model_correctness_test.py index d57599d7b95c..e50a83f813d7 100644 --- a/examples/android/tflite_convertor/tfltransfer/model_correctness_test.py +++ b/examples/android/tflite_convertor/tfltransfer/model_correctness_test.py @@ -14,9 +14,7 @@ """End-to-end tests that check model correctness.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import os import tempfile @@ -27,10 +25,7 @@ from tensorflow.compat import v1 as tfv1 # pylint: disable=g-bad-import-order -from tfltransfer import bases -from tfltransfer import optimizers -from tfltransfer import heads -from tfltransfer import tflite_transfer_converter +from tfltransfer import bases, heads, optimizers, tflite_transfer_converter # pylint: enable=g-bad-import-order diff --git a/examples/android/tflite_convertor/tfltransfer/optimizers/adam.py b/examples/android/tflite_convertor/tfltransfer/optimizers/adam.py index 2fe4e1442bb3..1351a0172641 100644 --- a/examples/android/tflite_convertor/tfltransfer/optimizers/adam.py +++ b/examples/android/tflite_convertor/tfltransfer/optimizers/adam.py @@ -14,9 +14,7 @@ """Adam optimizer implementation for transfer learning models.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import tensorflow as tf import tensorflow.compat.v1 as tfv1 diff --git a/examples/android/tflite_convertor/tfltransfer/optimizers/sgd.py b/examples/android/tflite_convertor/tfltransfer/optimizers/sgd.py index 09d22ba2fcad..729af1904103 100644 --- a/examples/android/tflite_convertor/tfltransfer/optimizers/sgd.py +++ b/examples/android/tflite_convertor/tfltransfer/optimizers/sgd.py @@ -14,9 +14,7 @@ """SGD optimizer implementation for transfer learning models.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import tensorflow as tf import tensorflow.compat.v1 as tfv1 diff --git a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_convert.py b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_convert.py index 93dcfd8a67d5..383b441b17ef 100644 --- a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_convert.py +++ b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_convert.py @@ -17,17 +17,12 @@ """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import argparse # pylint: disable=g-bad-import-order -from tfltransfer import bases -from tfltransfer import heads -from tfltransfer import optimizers -from tfltransfer import tflite_transfer_converter +from tfltransfer import bases, heads, optimizers, tflite_transfer_converter # pylint: enable=g-bad-import-order diff --git a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter.py b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter.py index 38d9493b7617..7e07953d36a9 100644 --- a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter.py +++ b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter.py @@ -19,9 +19,7 @@ """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import os diff --git a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter_test.py b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter_test.py index fa7e53c097bc..8b0c87719092 100644 --- a/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter_test.py +++ b/examples/android/tflite_convertor/tfltransfer/tflite_transfer_converter_test.py @@ -14,9 +14,7 @@ """Tests for tflite_transfer_converter.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import tempfile import unittest @@ -27,10 +25,7 @@ from tensorflow.keras.regularizers import l2 # pylint: disable=g-bad-import-order -from tfltransfer import bases -from tfltransfer import heads -from tfltransfer import optimizers -from tfltransfer import tflite_transfer_converter +from tfltransfer import bases, heads, optimizers, tflite_transfer_converter # pylint: enable=g-bad-import-order diff --git a/examples/android/tflite_convertor/tfltransfer/utils.py b/examples/android/tflite_convertor/tfltransfer/utils.py index 5648a449c7e7..c0f61b4e4aad 100644 --- a/examples/android/tflite_convertor/tfltransfer/utils.py +++ b/examples/android/tflite_convertor/tfltransfer/utils.py @@ -14,9 +14,7 @@ """Helper utilities for various parts of the converter.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function from tensorflow.compat import v1 as tfv1 diff --git a/examples/app-pytorch/client.py b/examples/app-pytorch/client.py index eb84968bb986..4168baedea5f 100644 --- a/examples/app-pytorch/client.py +++ b/examples/app-pytorch/client.py @@ -1,15 +1,6 @@ from flwr.client import ClientApp, NumPyClient -from task import ( - Net, - DEVICE, - load_data, - get_weights, - set_weights, - train, - test, -) - +from task import DEVICE, Net, get_weights, load_data, set_weights, test, train # Load model and data (simple CNN, CIFAR-10) net = Net().to(DEVICE) diff --git a/examples/app-pytorch/client_low_level.py b/examples/app-pytorch/client_low_level.py index 19268ff84ba4..538b5b4e88ff 100644 --- a/examples/app-pytorch/client_low_level.py +++ b/examples/app-pytorch/client_low_level.py @@ -1,5 +1,5 @@ from flwr.client import ClientApp -from flwr.common import Message, Context +from flwr.common import Context, Message def hello_world_mod(msg, ctx, call_next) -> Message: diff --git a/examples/app-pytorch/server.py b/examples/app-pytorch/server.py index 0b4ad1ddba46..0beb3d811346 100644 --- a/examples/app-pytorch/server.py +++ b/examples/app-pytorch/server.py @@ -1,8 +1,8 @@ from typing import List, Tuple +from flwr.common import Metrics, ndarrays_to_parameters from flwr.server import ServerApp, ServerConfig from flwr.server.strategy import FedAvg -from flwr.common import Metrics, ndarrays_to_parameters from task import Net, get_weights diff --git a/examples/app-pytorch/server_custom.py b/examples/app-pytorch/server_custom.py index 67c1bce99c55..4c9b7f868f31 100644 --- a/examples/app-pytorch/server_custom.py +++ b/examples/app-pytorch/server_custom.py @@ -1,19 +1,19 @@ -from typing import List, Tuple, Dict import random import time +from typing import Dict, List, Tuple import flwr as fl from flwr.common import ( + DEFAULT_TTL, + Code, Context, FitIns, - ndarrays_to_parameters, - parameters_to_ndarrays, - NDArrays, - Code, Message, MessageType, Metrics, - DEFAULT_TTL, + NDArrays, + ndarrays_to_parameters, + parameters_to_ndarrays, ) from flwr.common.recordset_compat import fitins_to_recordset, recordset_to_fitres from flwr.server import Driver, History diff --git a/examples/app-pytorch/server_low_level.py b/examples/app-pytorch/server_low_level.py index 7ab79a4a04c8..d11ae446cc28 100644 --- a/examples/app-pytorch/server_low_level.py +++ b/examples/app-pytorch/server_low_level.py @@ -1,20 +1,19 @@ -from typing import List, Tuple, Dict import random import time +from typing import Dict, List, Tuple import flwr as fl from flwr.common import ( + DEFAULT_TTL, Context, - NDArrays, Message, MessageType, Metrics, + NDArrays, RecordSet, - DEFAULT_TTL, ) from flwr.server import Driver - # Run via `flower-server-app server:app` app = fl.server.ServerApp() diff --git a/examples/app-pytorch/server_workflow.py b/examples/app-pytorch/server_workflow.py index 270e1ef2c7cd..5ebca1e40be0 100644 --- a/examples/app-pytorch/server_workflow.py +++ b/examples/app-pytorch/server_workflow.py @@ -1,11 +1,11 @@ from typing import List, Tuple -from task import Net, get_weights - import flwr as fl from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.server import Driver, LegacyContext +from task import Net, get_weights + # Define metric aggregation function def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: diff --git a/examples/app-pytorch/task.py b/examples/app-pytorch/task.py index 240f290df320..1fd61966a61a 100644 --- a/examples/app-pytorch/task.py +++ b/examples/app-pytorch/task.py @@ -9,7 +9,6 @@ from torchvision.datasets import CIFAR10 from torchvision.transforms import Compose, Normalize, ToTensor - DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/custom-metrics/custommetrics_example/client_app.py b/examples/custom-metrics/custommetrics_example/client_app.py index 2c4f7e2adbfa..babba6b0b9d6 100644 --- a/examples/custom-metrics/custommetrics_example/client_app.py +++ b/examples/custom-metrics/custommetrics_example/client_app.py @@ -3,11 +3,11 @@ import os import numpy as np -from custommetrics_example.task import eval_learning, get_model, load_data - from flwr.client import Client, ClientApp, NumPyClient from flwr.common import Context +from custommetrics_example.task import eval_learning, get_model, load_data + # Make TensorFlow log less verbose os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" diff --git a/examples/custom-metrics/custommetrics_example/server_app.py b/examples/custom-metrics/custommetrics_example/server_app.py index 4caa09843d1c..dda2db5cf2f4 100644 --- a/examples/custom-metrics/custommetrics_example/server_app.py +++ b/examples/custom-metrics/custommetrics_example/server_app.py @@ -1,12 +1,12 @@ """custommetrics_example: A Flower / TensorFlow app for custom metrics.""" import numpy as np -from custommetrics_example.task import get_model, get_parameters - from flwr.common import Context, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg +from custommetrics_example.task import get_model, get_parameters + # Define metrics aggregation function def average_metrics(metrics): diff --git a/examples/custom-mods/client.py b/examples/custom-mods/client.py index 614daef6bcf6..d59a55f6bf3d 100644 --- a/examples/custom-mods/client.py +++ b/examples/custom-mods/client.py @@ -5,21 +5,13 @@ import flwr as fl import tensorflow as tf import wandb -from flwr.common import ConfigsRecord from flwr.client.typing import ClientAppCallable, Mod +from flwr.common import ConfigsRecord +from flwr.common.constant import MessageType from flwr.common.context import Context from flwr.common.message import Message -from flwr.common.constant import MessageType -from task import ( - Net, - DEVICE, - load_data, - get_parameters, - set_parameters, - train, - test, -) +from task import DEVICE, Net, get_parameters, load_data, set_parameters, test, train class WBLoggingFilter(logging.Filter): diff --git a/examples/custom-mods/task.py b/examples/custom-mods/task.py index 276aace885df..331bd324061d 100644 --- a/examples/custom-mods/task.py +++ b/examples/custom-mods/task.py @@ -9,7 +9,6 @@ from torchvision.transforms import Compose, Normalize, ToTensor from tqdm import tqdm - warnings.filterwarnings("ignore", category=UserWarning) DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/embedded-devices/client_pytorch.py b/examples/embedded-devices/client_pytorch.py index 411052bfb1ea..0fee7a854d67 100644 --- a/examples/embedded-devices/client_pytorch.py +++ b/examples/embedded-devices/client_pytorch.py @@ -6,13 +6,12 @@ import torch import torch.nn as nn import torch.nn.functional as F +from flwr_datasets import FederatedDataset from torch.utils.data import DataLoader -from torchvision.transforms import Compose, Normalize, ToTensor from torchvision.models import mobilenet_v3_small +from torchvision.transforms import Compose, Normalize, ToTensor from tqdm import tqdm -from flwr_datasets import FederatedDataset - parser = argparse.ArgumentParser(description="Flower Embedded devices") parser.add_argument( "--server_address", diff --git a/examples/embedded-devices/client_tf.py b/examples/embedded-devices/client_tf.py index 3df75f76312b..524404b3ef8b 100644 --- a/examples/embedded-devices/client_tf.py +++ b/examples/embedded-devices/client_tf.py @@ -1,12 +1,11 @@ -import math import argparse +import math import warnings import flwr as fl import tensorflow as tf -from tensorflow import keras as keras - from flwr_datasets import FederatedDataset +from tensorflow import keras as keras parser = argparse.ArgumentParser(description="Flower Embedded devices") parser.add_argument( diff --git a/examples/embedded-devices/server.py b/examples/embedded-devices/server.py index 2a6194aa5088..49c72720f02a 100644 --- a/examples/embedded-devices/server.py +++ b/examples/embedded-devices/server.py @@ -4,7 +4,6 @@ import flwr as fl from flwr.common import Metrics - parser = argparse.ArgumentParser(description="Flower Embedded devices") parser.add_argument( "--server_address", diff --git a/examples/federated-kaplan-meier-fitter/server.py b/examples/federated-kaplan-meier-fitter/server.py index 141504ab59c0..e1f84a961bf1 100644 --- a/examples/federated-kaplan-meier-fitter/server.py +++ b/examples/federated-kaplan-meier-fitter/server.py @@ -14,18 +14,18 @@ # ============================================================================== """Strategy that supports many univariate fitters from lifelines library.""" -from typing import Dict, List, Optional, Tuple, Union, Any +from typing import Any, Dict, List, Optional, Tuple, Union -import numpy as np import flwr as fl import matplotlib.pyplot as plt +import numpy as np from flwr.common import ( + EvaluateIns, + EvaluateRes, FitIns, + FitRes, Parameters, Scalar, - EvaluateRes, - EvaluateIns, - FitRes, parameters_to_ndarrays, ) from flwr.server.client_manager import ClientManager diff --git a/examples/fl-dp-sa/fl_dp_sa/client.py b/examples/fl-dp-sa/fl_dp_sa/client.py index 104264158833..b3b02c6e9d61 100644 --- a/examples/fl-dp-sa/fl_dp_sa/client.py +++ b/examples/fl-dp-sa/fl_dp_sa/client.py @@ -5,7 +5,6 @@ from fl_dp_sa.task import DEVICE, Net, get_weights, load_data, set_weights, test, train - # Load model and data (simple CNN, CIFAR-10) net = Net().to(DEVICE) diff --git a/examples/fl-dp-sa/fl_dp_sa/server.py b/examples/fl-dp-sa/fl_dp_sa/server.py index 76ff0a9491ff..3ec0ba757b0d 100644 --- a/examples/fl-dp-sa/fl_dp_sa/server.py +++ b/examples/fl-dp-sa/fl_dp_sa/server.py @@ -2,12 +2,9 @@ from typing import List, Tuple -from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig from flwr.common import Context, Metrics, ndarrays_to_parameters -from flwr.server.strategy import ( - DifferentialPrivacyClientSideFixedClipping, - FedAvg, -) +from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig +from flwr.server.strategy import DifferentialPrivacyClientSideFixedClipping, FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow from fl_dp_sa.task import Net, get_weights diff --git a/examples/fl-dp-sa/fl_dp_sa/task.py b/examples/fl-dp-sa/fl_dp_sa/task.py index 6a94571a2369..5b4fd7dee592 100644 --- a/examples/fl-dp-sa/fl_dp_sa/task.py +++ b/examples/fl-dp-sa/fl_dp_sa/task.py @@ -2,16 +2,15 @@ from collections import OrderedDict from logging import INFO -from flwr_datasets import FederatedDataset import torch import torch.nn as nn import torch.nn.functional as F from flwr.common.logger import log +from flwr_datasets import FederatedDataset from torch.utils.data import DataLoader from torchvision.transforms import Compose, Normalize, ToTensor - DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/fl-tabular/client.py b/examples/fl-tabular/client.py index 228183f4edc4..86b78fb21f0c 100644 --- a/examples/fl-tabular/client.py +++ b/examples/fl-tabular/client.py @@ -1,6 +1,7 @@ from flwr.client import Client, ClientApp, NumPyClient from flwr_datasets import FederatedDataset -from task import set_weights, get_weights, train, evaluate, IncomeClassifier, load_data + +from task import IncomeClassifier, evaluate, get_weights, load_data, set_weights, train NUMBER_OF_CLIENTS = 5 diff --git a/examples/fl-tabular/server.py b/examples/fl-tabular/server.py index 376726f832f7..6b33d74d5b69 100644 --- a/examples/fl-tabular/server.py +++ b/examples/fl-tabular/server.py @@ -1,6 +1,7 @@ from flwr.common import ndarrays_to_parameters from flwr.server import ServerApp, ServerConfig from flwr.server.strategy import FedAvg + from task import IncomeClassifier, get_weights net = IncomeClassifier(input_dim=14) diff --git a/examples/fl-tabular/task.py b/examples/fl-tabular/task.py index b07365c733d6..3dc32cfa7105 100644 --- a/examples/fl-tabular/task.py +++ b/examples/fl-tabular/task.py @@ -1,13 +1,14 @@ +from collections import OrderedDict + import torch import torch.nn as nn import torch.optim as optim -from torch.utils.data import TensorDataset, DataLoader -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import StandardScaler, OrdinalEncoder +from flwr_datasets import FederatedDataset from sklearn.compose import ColumnTransformer +from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline -from collections import OrderedDict -from flwr_datasets import FederatedDataset +from sklearn.preprocessing import OrdinalEncoder, StandardScaler +from torch.utils.data import DataLoader, TensorDataset def load_data(partition_id: int, fds: FederatedDataset): diff --git a/examples/flower-authentication/client.py b/examples/flower-authentication/client.py index 3c99d5a410c9..065acefb7bed 100644 --- a/examples/flower-authentication/client.py +++ b/examples/flower-authentication/client.py @@ -1,17 +1,9 @@ from typing import Dict -from flwr.common import NDArrays, Scalar -from flwr.client import ClientApp, NumPyClient -from task import ( - Net, - DEVICE, - load_data, - get_parameters, - set_parameters, - train, - test, -) +from flwr.client import ClientApp, NumPyClient +from flwr.common import NDArrays, Scalar +from task import DEVICE, Net, get_parameters, load_data, set_parameters, test, train # Load model and data (simple CNN, CIFAR-10) net = Net().to(DEVICE) diff --git a/examples/flower-authentication/server.py b/examples/flower-authentication/server.py index d88dc1d1a641..44908a0d9fc4 100644 --- a/examples/flower-authentication/server.py +++ b/examples/flower-authentication/server.py @@ -2,8 +2,8 @@ import flwr as fl from flwr.common import Metrics -from flwr.server.strategy.fedavg import FedAvg from flwr.server import ServerApp +from flwr.server.strategy.fedavg import FedAvg # Define metric aggregation function diff --git a/examples/flower-authentication/task.py b/examples/flower-authentication/task.py index 276aace885df..331bd324061d 100644 --- a/examples/flower-authentication/task.py +++ b/examples/flower-authentication/task.py @@ -9,7 +9,6 @@ from torchvision.transforms import Compose, Normalize, ToTensor from tqdm import tqdm - warnings.filterwarnings("ignore", category=UserWarning) DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index a332ffb9eca1..0f1b594317fa 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -3,16 +3,15 @@ from logging import DEBUG from typing import List, Tuple -from secaggexample.task import get_weights, make_net -from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs - from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.common.logger import update_console_handler - from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig from flwr.server.strategy import FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow +from secaggexample.task import get_weights, make_net +from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs + # Define metric aggregation function def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index b2e457484de7..75f43d580c4d 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -2,8 +2,6 @@ from logging import INFO -from secaggexample.task import get_weights, make_net - import flwr.common.recordset_compat as compat from flwr.common import Context, log, parameters_to_ndarrays from flwr.common.secure_aggregation.quantization import quantize @@ -14,6 +12,8 @@ WorkflowState, ) +from secaggexample.task import get_weights, make_net + class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): """The SecAggPlusWorkflow augmented for this example. diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py index eac831ad1932..3d93510b3d0e 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py @@ -1,11 +1,11 @@ from collections import OrderedDict from typing import Dict, Tuple -from flwr.common import NDArrays, Scalar -import torch import flwr as fl +import torch +from flwr.common import NDArrays, Scalar -from model import Net, train, test +from model import Net, test, train class FlowerClient(fl.client.NumPyClient): diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py index 1085ede22c9a..a805906b8d42 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py @@ -1,7 +1,7 @@ import torch -from torch.utils.data import random_split, DataLoader -from torchvision.transforms import ToTensor, Normalize, Compose +from torch.utils.data import DataLoader, random_split from torchvision.datasets import MNIST +from torchvision.transforms import Compose, Normalize, ToTensor def get_mnist(data_path: str = "./data"): diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py index f8124b9353f7..1373f24fbb11 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py @@ -1,15 +1,14 @@ import pickle from pathlib import Path +import flwr as fl import hydra from hydra.core.hydra_config import HydraConfig from omegaconf import DictConfig, OmegaConf -import flwr as fl - -from dataset import prepare_dataset from client import generate_client_fn -from server import get_on_fit_config, get_evaluate_fn +from dataset import prepare_dataset +from server import get_evaluate_fn, get_on_fit_config # A decorator for Hydra. This tells hydra to by default load the config in conf/base.yaml diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/server.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/server.py index 33f618785d56..93350ae2d1ba 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/server.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-I/server.py @@ -1,9 +1,7 @@ from collections import OrderedDict - -from omegaconf import DictConfig - import torch +from omegaconf import DictConfig from model import Net, test diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/client.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/client.py index 7da9547d7362..098cac293d94 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/client.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/client.py @@ -1,14 +1,12 @@ from collections import OrderedDict from typing import Dict, Tuple -from flwr.common import NDArrays, Scalar - - -from hydra.utils import instantiate -import torch import flwr as fl +import torch +from flwr.common import NDArrays, Scalar +from hydra.utils import instantiate -from model import train, test +from model import test, train class FlowerClient(fl.client.NumPyClient): diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/dataset.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/dataset.py index a80e1c78098e..fb5d8504ed65 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/dataset.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/dataset.py @@ -1,7 +1,7 @@ import torch -from torch.utils.data import random_split, DataLoader -from torchvision.transforms import ToTensor, Normalize, Compose +from torch.utils.data import DataLoader, random_split from torchvision.datasets import MNIST +from torchvision.transforms import Compose, Normalize, ToTensor def get_mnist(data_path: str = "./data"): diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/main.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/main.py index d43dd6d50787..6da664df1203 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/main.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/main.py @@ -1,17 +1,15 @@ import pickle from pathlib import Path +import flwr as fl import hydra -from hydra.utils import instantiate, call from hydra.core.hydra_config import HydraConfig +from hydra.utils import call, instantiate from omegaconf import DictConfig, OmegaConf -import flwr as fl - -from dataset import prepare_dataset from client import generate_client_fn -from server import get_on_fit_config, get_evalulate_fn - +from dataset import prepare_dataset +from server import get_evalulate_fn, get_on_fit_config # !!!! The code in this directory is the result of adpating the project first shown # in to make better use of Hydra's config system. It is recommended to first diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/model.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/model.py index 6dc1782f4dc2..f57bc9b5d100 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/model.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/model.py @@ -1,7 +1,6 @@ import torch import torch.nn as nn import torch.nn.functional as F - from flwr.common.parameter import ndarrays_to_parameters diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/server.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/server.py index 8901370e1a06..f1f8293cc6fb 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/server.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/server.py @@ -1,10 +1,8 @@ from collections import OrderedDict - -from omegaconf import DictConfig -from hydra.utils import instantiate - import torch +from hydra.utils import instantiate +from omegaconf import DictConfig from model import Net, test diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-II/toy.py b/examples/flower-simulation-step-by-step-pytorch/Part-II/toy.py index 7a6c057668c4..0bae932e3bce 100644 --- a/examples/flower-simulation-step-by-step-pytorch/Part-II/toy.py +++ b/examples/flower-simulation-step-by-step-pytorch/Part-II/toy.py @@ -1,7 +1,6 @@ import hydra -from omegaconf import DictConfig, OmegaConf - from hydra.utils import call, instantiate +from omegaconf import DictConfig, OmegaConf def function_test(x: int, y: int): diff --git a/examples/flower-via-docker-compose/client.py b/examples/flower-via-docker-compose/client.py index c894143532a1..33ed64d3270d 100644 --- a/examples/flower-via-docker-compose/client.py +++ b/examples/flower-via-docker-compose/client.py @@ -1,10 +1,11 @@ -import os import argparse +import logging +import os + import flwr as fl import tensorflow as tf -import logging from helpers.load_data import load_data -import os + from model.model import Model logging.basicConfig(level=logging.INFO) # Configure logging diff --git a/examples/flower-via-docker-compose/helpers/generate_docker_compose.py b/examples/flower-via-docker-compose/helpers/generate_docker_compose.py index cde553a95e68..4067439a4544 100644 --- a/examples/flower-via-docker-compose/helpers/generate_docker_compose.py +++ b/examples/flower-via-docker-compose/helpers/generate_docker_compose.py @@ -1,5 +1,5 @@ -import random import argparse +import random parser = argparse.ArgumentParser(description="Generated Docker Compose") parser.add_argument( diff --git a/examples/flower-via-docker-compose/helpers/load_data.py b/examples/flower-via-docker-compose/helpers/load_data.py index b7d6b0de26c5..aecb130a4eb5 100644 --- a/examples/flower-via-docker-compose/helpers/load_data.py +++ b/examples/flower-via-docker-compose/helpers/load_data.py @@ -1,7 +1,8 @@ +import logging + import numpy as np import tensorflow as tf from flwr_datasets import FederatedDataset -import logging logging.basicConfig(level=logging.INFO) # Configure logging logger = logging.getLogger(__name__) # Create logger for the module diff --git a/examples/flower-via-docker-compose/server.py b/examples/flower-via-docker-compose/server.py index 99d1a7ef7399..fd5292dd061a 100644 --- a/examples/flower-via-docker-compose/server.py +++ b/examples/flower-via-docker-compose/server.py @@ -1,8 +1,10 @@ import argparse -import flwr as fl import logging + +import flwr as fl + +from prometheus_client import Gauge, start_http_server from strategy.strategy import FedCustom -from prometheus_client import start_http_server, Gauge # Initialize Logging logging.basicConfig(level=logging.INFO) diff --git a/examples/flower-via-docker-compose/strategy/strategy.py b/examples/flower-via-docker-compose/strategy/strategy.py index 9471a99f037f..a2170ed86b49 100644 --- a/examples/flower-via-docker-compose/strategy/strategy.py +++ b/examples/flower-via-docker-compose/strategy/strategy.py @@ -1,9 +1,11 @@ +import logging from typing import Dict, List, Optional, Tuple, Union -from flwr.common import Scalar, EvaluateRes + +import flwr as fl +from flwr.common import EvaluateRes, Scalar from flwr.server.client_proxy import ClientProxy from flwr.server.strategy.aggregate import aggregate, weighted_loss_avg -import flwr as fl -import logging + from prometheus_client import Gauge logging.basicConfig(level=logging.INFO) # Configure logging diff --git a/examples/ios/server.py b/examples/ios/server.py index 521297c9905e..c1c6780840e9 100644 --- a/examples/ios/server.py +++ b/examples/ios/server.py @@ -1,5 +1,6 @@ -import flwr import argparse + +import flwr import numpy as np diff --git a/examples/llm-flowertune/app.py b/examples/llm-flowertune/app.py index e04ad8715de6..db6595c94d31 100644 --- a/examples/llm-flowertune/app.py +++ b/examples/llm-flowertune/app.py @@ -1,14 +1,13 @@ import os import warnings -from hydra import compose, initialize import flwr as fl from flwr_datasets import FederatedDataset +from hydra import compose, initialize -from dataset import get_tokenizer_and_data_collator_and_propt_formatting from client import gen_client_fn -from utils import get_on_fit_config, fit_weighted_average - +from dataset import get_tokenizer_and_data_collator_and_propt_formatting +from utils import fit_weighted_average, get_on_fit_config warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/llm-flowertune/client.py b/examples/llm-flowertune/client.py index 28b324ba5bf1..c81333f664b3 100644 --- a/examples/llm-flowertune/client.py +++ b/examples/llm-flowertune/client.py @@ -5,11 +5,11 @@ import torch from flwr.common.typing import NDArrays, Scalar from omegaconf import DictConfig -from trl import SFTTrainer -from transformers import TrainingArguments from peft import get_peft_model_state_dict, set_peft_model_state_dict +from transformers import TrainingArguments +from trl import SFTTrainer -from models import get_model, cosine_annealing +from models import cosine_annealing, get_model # pylint: disable=too-many-arguments diff --git a/examples/llm-flowertune/main.py b/examples/llm-flowertune/main.py index 2d03e9cbcae5..ec8308601efb 100644 --- a/examples/llm-flowertune/main.py +++ b/examples/llm-flowertune/main.py @@ -1,18 +1,16 @@ -import warnings import pickle +import warnings import flwr as fl -from flwr_datasets import FederatedDataset - import hydra +from flwr_datasets import FederatedDataset from hydra.core.hydra_config import HydraConfig from hydra.utils import instantiate from omegaconf import DictConfig, OmegaConf -from dataset import get_tokenizer_and_data_collator_and_propt_formatting -from utils import get_on_fit_config, fit_weighted_average, get_evaluate_fn from client import gen_client_fn - +from dataset import get_tokenizer_and_data_collator_and_propt_formatting +from utils import fit_weighted_average, get_evaluate_fn, get_on_fit_config warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/llm-flowertune/models.py b/examples/llm-flowertune/models.py index 78eef75d10d2..f32c800cf2c1 100644 --- a/examples/llm-flowertune/models.py +++ b/examples/llm-flowertune/models.py @@ -1,11 +1,10 @@ +import math + import torch from omegaconf import DictConfig -from transformers import AutoModelForCausalLM -from transformers import BitsAndBytesConfig -from peft import get_peft_model, LoraConfig +from peft import LoraConfig, get_peft_model from peft.utils import prepare_model_for_kbit_training - -import math +from transformers import AutoModelForCausalLM, BitsAndBytesConfig def cosine_annealing( diff --git a/examples/llm-flowertune/test.py b/examples/llm-flowertune/test.py index 652bb9aafcf5..fa8aa26100a8 100644 --- a/examples/llm-flowertune/test.py +++ b/examples/llm-flowertune/test.py @@ -1,11 +1,11 @@ # This python file is adapted from https://github.com/lm-sys/FastChat/blob/main/fastchat/llm_judge/gen_model_answer.py import argparse + import torch +from fastchat.conversation import get_conv_template from peft import AutoPeftModelForCausalLM from transformers import AutoTokenizer -from fastchat.conversation import get_conv_template - parser = argparse.ArgumentParser() parser.add_argument("--peft-path", type=str, default=None) diff --git a/examples/opacus/client.py b/examples/opacus/client.py index 51c1e1cfa667..2771a5d78bcc 100644 --- a/examples/opacus/client.py +++ b/examples/opacus/client.py @@ -2,17 +2,16 @@ import warnings from collections import OrderedDict -from flwr_datasets import FederatedDataset -from flwr.client import NumPyClient, ClientApp import torch import torch.nn as nn import torch.nn.functional as F +from flwr.client import ClientApp, NumPyClient +from flwr_datasets import FederatedDataset +from opacus import PrivacyEngine from torch.utils.data import DataLoader from torchvision.transforms import Compose, Normalize, ToTensor from tqdm import tqdm -from opacus import PrivacyEngine - warnings.filterwarnings("ignore", category=UserWarning) DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/opacus/server.py b/examples/opacus/server.py index a206c48307e2..68c1c027d3d6 100644 --- a/examples/opacus/server.py +++ b/examples/opacus/server.py @@ -1,9 +1,9 @@ from typing import List, Tuple import flwr as fl -from flwr.server.strategy import FedAvg from flwr.common import Metrics from flwr.server import ServerApp, ServerConfig +from flwr.server.strategy import FedAvg def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: diff --git a/examples/pytorch-federated-variational-autoencoder/client.py b/examples/pytorch-federated-variational-autoencoder/client.py index fc71f7e70c0b..9fa4707a7a45 100644 --- a/examples/pytorch-federated-variational-autoencoder/client.py +++ b/examples/pytorch-federated-variational-autoencoder/client.py @@ -1,13 +1,13 @@ from collections import OrderedDict +import flwr as fl import torch import torch.nn.functional as F import torchvision.transforms as transforms -from models import Net from torch.utils.data import DataLoader from torchvision.datasets import CIFAR10 -import flwr as fl +from models import Net DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/pytorch-from-centralized-to-federated/cifar.py b/examples/pytorch-from-centralized-to-federated/cifar.py index c592b63b0042..fdd3ea865c13 100644 --- a/examples/pytorch-from-centralized-to-federated/cifar.py +++ b/examples/pytorch-from-centralized-to-federated/cifar.py @@ -15,11 +15,10 @@ import torch import torch.nn as nn import torch.nn.functional as F +from flwr_datasets import FederatedDataset from torch import Tensor from torch.utils.data import DataLoader -from torchvision.transforms import Compose, ToTensor, Normalize - -from flwr_datasets import FederatedDataset +from torchvision.transforms import Compose, Normalize, ToTensor # pylint: disable=unsubscriptable-object diff --git a/examples/pytorch-from-centralized-to-federated/client.py b/examples/pytorch-from-centralized-to-federated/client.py index 9df4739e0aab..845f7ae2b5b2 100644 --- a/examples/pytorch-from-centralized-to-federated/client.py +++ b/examples/pytorch-from-centralized-to-federated/client.py @@ -4,14 +4,13 @@ from collections import OrderedDict from typing import Dict, List, Tuple +import cifar +import flwr as fl import numpy as np import torch from datasets.utils.logging import disable_progress_bar from torch.utils.data import DataLoader -import cifar -import flwr as fl - disable_progress_bar() diff --git a/examples/quickstart-cpp/fedavg_cpp.py b/examples/quickstart-cpp/fedavg_cpp.py index cd62d07bb848..3488bfb1f2e4 100644 --- a/examples/quickstart-cpp/fedavg_cpp.py +++ b/examples/quickstart-cpp/fedavg_cpp.py @@ -3,15 +3,9 @@ from typing import Callable, Dict, List, Optional, Tuple, Union import numpy as np -from flwr.server.strategy import FedAvg -from flwr.common import ( - EvaluateRes, - FitRes, - Parameters, - Scalar, - NDArrays, -) +from flwr.common import EvaluateRes, FitRes, NDArrays, Parameters, Scalar from flwr.server.client_proxy import ClientProxy +from flwr.server.strategy import FedAvg from flwr.server.strategy.aggregate import aggregate, weighted_loss_avg diff --git a/examples/quickstart-fastai/fastai_example/client_app.py b/examples/quickstart-fastai/fastai_example/client_app.py index a40f3d5c176d..0094d6fc9f56 100644 --- a/examples/quickstart-fastai/fastai_example/client_app.py +++ b/examples/quickstart-fastai/fastai_example/client_app.py @@ -7,11 +7,10 @@ from fastai.losses import CrossEntropyLossFlat from fastai.vision.all import error_rate, squeezenet1_1 from fastai.vision.data import DataLoaders - from flwr.client import Client, ClientApp, NumPyClient from flwr.common import Context -from fastai_example.task import load_data, set_params, get_params +from fastai_example.task import get_params, load_data, set_params warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/quickstart-huggingface/client.py b/examples/quickstart-huggingface/client.py index a9d48bfa8f13..b880119d1c7c 100644 --- a/examples/quickstart-huggingface/client.py +++ b/examples/quickstart-huggingface/client.py @@ -5,12 +5,14 @@ import flwr as fl import torch from evaluate import load as load_metric +from flwr_datasets import FederatedDataset from torch.optim import AdamW from torch.utils.data import DataLoader -from transformers import AutoModelForSequenceClassification -from transformers import AutoTokenizer, DataCollatorWithPadding - -from flwr_datasets import FederatedDataset +from transformers import ( + AutoModelForSequenceClassification, + AutoTokenizer, + DataCollatorWithPadding, +) warnings.filterwarnings("ignore", category=UserWarning) DEVICE = torch.device("cpu") diff --git a/examples/quickstart-huggingface/server.py b/examples/quickstart-huggingface/server.py index aab87982076e..4eeb9da7da75 100644 --- a/examples/quickstart-huggingface/server.py +++ b/examples/quickstart-huggingface/server.py @@ -1,6 +1,5 @@ import flwr as fl - if __name__ == "__main__": # Define strategy strategy = fl.server.strategy.FedAvg( diff --git a/examples/quickstart-jax/client.py b/examples/quickstart-jax/client.py index 2257a3d6daa3..4a2aaf0e5a93 100644 --- a/examples/quickstart-jax/client.py +++ b/examples/quickstart-jax/client.py @@ -1,14 +1,12 @@ """Flower client example using JAX for linear regression.""" -from typing import Dict, List, Tuple, Callable +from typing import Callable, Dict, List, Tuple import flwr as fl -import numpy as np import jax import jax.numpy as jnp - import jax_training - +import numpy as np # Load data and determine model shape train_x, train_y, test_x, test_y = jax_training.load_data() diff --git a/examples/quickstart-jax/jax_training.py b/examples/quickstart-jax/jax_training.py index a2e23a0927bc..f57db75d5963 100644 --- a/examples/quickstart-jax/jax_training.py +++ b/examples/quickstart-jax/jax_training.py @@ -7,12 +7,13 @@ please read the JAX documentation or the mentioned tutorial. """ -from typing import Dict, List, Tuple, Callable +from typing import Callable, Dict, List, Tuple + import jax import jax.numpy as jnp +import numpy as np from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split -import numpy as np key = jax.random.PRNGKey(0) diff --git a/examples/quickstart-mlcube/client.py b/examples/quickstart-mlcube/client.py index 46ddd45f52ce..458da07606ba 100644 --- a/examples/quickstart-mlcube/client.py +++ b/examples/quickstart-mlcube/client.py @@ -1,6 +1,8 @@ import os import sys + import flwr as fl + import mlcube_utils as mlcube diff --git a/examples/quickstart-mlcube/mlcube_utils.py b/examples/quickstart-mlcube/mlcube_utils.py index 1db5c446d681..8d72d43116d1 100644 --- a/examples/quickstart-mlcube/mlcube_utils.py +++ b/examples/quickstart-mlcube/mlcube_utils.py @@ -1,12 +1,11 @@ +import json import os -import sys import subprocess -import tensorflow as tf -import json +import sys +import tensorflow as tf from flwr.common import ndarrays_to_parameters - MODULE_PATH = os.path.abspath(__file__) MODULE_DIR = os.path.dirname(MODULE_PATH) MLCUBE_DIR = os.path.join(MODULE_DIR, "mlcube") diff --git a/examples/quickstart-mlx/mlxexample/server_app.py b/examples/quickstart-mlx/mlxexample/server_app.py index 7a50dfd41305..7c93f38ed449 100644 --- a/examples/quickstart-mlx/mlxexample/server_app.py +++ b/examples/quickstart-mlx/mlxexample/server_app.py @@ -5,7 +5,6 @@ from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg - from mlxexample.task import MLP, get_params diff --git a/examples/quickstart-monai/client.py b/examples/quickstart-monai/client.py index 0ed943da83cc..1401928af1ff 100644 --- a/examples/quickstart-monai/client.py +++ b/examples/quickstart-monai/client.py @@ -2,12 +2,12 @@ import warnings from collections import OrderedDict +import flwr as fl import torch -from data import load_data -from model import test, train from monai.networks.nets.densenet import DenseNet121 -import flwr as fl +from data import load_data +from model import test, train warnings.filterwarnings("ignore", category=UserWarning) DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") diff --git a/examples/quickstart-pandas/client.py b/examples/quickstart-pandas/client.py index c52b7c65b04c..5a501e3517e6 100644 --- a/examples/quickstart-pandas/client.py +++ b/examples/quickstart-pandas/client.py @@ -1,14 +1,11 @@ import argparse from typing import Dict, List, Tuple +import flwr as fl import numpy as np import pandas as pd - -import flwr as fl - from flwr_datasets import FederatedDataset - column_names = ["sepal_length", "sepal_width"] diff --git a/examples/quickstart-pandas/server.py b/examples/quickstart-pandas/server.py index af4c2a796788..76cbd6194579 100644 --- a/examples/quickstart-pandas/server.py +++ b/examples/quickstart-pandas/server.py @@ -1,8 +1,7 @@ from typing import Dict, List, Optional, Tuple, Union -import numpy as np - import flwr as fl +import numpy as np from flwr.common import ( EvaluateIns, EvaluateRes, diff --git a/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py b/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py index f89aa8bb79e4..8d0f2266ab2f 100644 --- a/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py +++ b/examples/quickstart-pytorch-lightning/pytorchlightning_example/server_app.py @@ -3,7 +3,6 @@ from flwr.common import Context, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg - from pytorchlightning_example.task import LitAutoEncoder, get_parameters diff --git a/examples/quickstart-pytorch/pytorchexample/client_app.py b/examples/quickstart-pytorch/pytorchexample/client_app.py index 35ab41abdaf4..1052741710db 100644 --- a/examples/quickstart-pytorch/pytorchexample/client_app.py +++ b/examples/quickstart-pytorch/pytorchexample/client_app.py @@ -1,17 +1,10 @@ """pytorchexample: A Flower / PyTorch app.""" import torch -from flwr.client import NumPyClient, ClientApp +from flwr.client import ClientApp, NumPyClient from flwr.common import Context -from pytorchexample.task import ( - Net, - load_data, - get_weights, - set_weights, - train, - test, -) +from pytorchexample.task import Net, get_weights, load_data, set_weights, test, train # Define Flower Client diff --git a/examples/quickstart-pytorch/pytorchexample/server_app.py b/examples/quickstart-pytorch/pytorchexample/server_app.py index faf3293fe272..834725976d1a 100644 --- a/examples/quickstart-pytorch/pytorchexample/server_app.py +++ b/examples/quickstart-pytorch/pytorchexample/server_app.py @@ -1,7 +1,8 @@ """pytorchexample: A Flower / PyTorch app.""" from typing import List, Tuple -from flwr.common import Context, ndarrays_to_parameters, Metrics + +from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg diff --git a/examples/quickstart-pytorch/pytorchexample/task.py b/examples/quickstart-pytorch/pytorchexample/task.py index ade5856a7b99..8e0808871616 100644 --- a/examples/quickstart-pytorch/pytorchexample/task.py +++ b/examples/quickstart-pytorch/pytorchexample/task.py @@ -5,10 +5,10 @@ import torch import torch.nn as nn import torch.nn.functional as F -from torch.utils.data import DataLoader -from torchvision.transforms import Compose, Normalize, ToTensor from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor class Net(nn.Module): diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py b/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py index fb1e581c978a..bf0c4069c7f8 100644 --- a/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py +++ b/examples/quickstart-sklearn-tabular/sklearnexample/client_app.py @@ -2,16 +2,16 @@ import warnings -from sklearn.metrics import log_loss +from flwr.client import ClientApp, NumPyClient from flwr.common import Context -from flwr.client import NumPyClient, ClientApp +from sklearn.metrics import log_loss from sklearnexample.task import ( + UNIQUE_LABELS, create_log_reg_and_instantiate_parameters, - set_model_params, get_model_parameters, load_data, - UNIQUE_LABELS, + set_model_params, ) diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py b/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py index a99a74a140c0..1bef9d5ed49c 100644 --- a/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py +++ b/examples/quickstart-sklearn-tabular/sklearnexample/server_app.py @@ -1,9 +1,9 @@ """sklearnexample: A Flower / sklearn app.""" -from typing import List, Tuple, Dict +from typing import Dict, List, Tuple -from flwr.common import Metrics, Scalar, Context, ndarrays_to_parameters -from flwr.server import ServerAppComponents, ServerConfig, ServerApp +from flwr.common import Context, Metrics, Scalar, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg from sklearnexample.task import ( diff --git a/examples/quickstart-sklearn-tabular/sklearnexample/task.py b/examples/quickstart-sklearn-tabular/sklearnexample/task.py index db4d70761edd..71dcbc8c474f 100644 --- a/examples/quickstart-sklearn-tabular/sklearnexample/task.py +++ b/examples/quickstart-sklearn-tabular/sklearnexample/task.py @@ -1,9 +1,8 @@ import numpy as np -from sklearn.linear_model import LogisticRegression - from flwr.common import NDArrays from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner +from sklearn.linear_model import LogisticRegression # This information is needed to create a correct scikit-learn model UNIQUE_LABELS = [0, 1, 2] diff --git a/examples/quickstart-tabnet/client.py b/examples/quickstart-tabnet/client.py index 2289b1b55b3d..4da5a394a199 100644 --- a/examples/quickstart-tabnet/client.py +++ b/examples/quickstart-tabnet/client.py @@ -1,8 +1,9 @@ import os + import flwr as fl +import tabnet import tensorflow as tf import tensorflow_datasets as tfds -import tabnet train_size = 125 BATCH_SIZE = 50 diff --git a/examples/quickstart-tabnet/server.py b/examples/quickstart-tabnet/server.py index 39c350388c1b..a99eb48059bc 100644 --- a/examples/quickstart-tabnet/server.py +++ b/examples/quickstart-tabnet/server.py @@ -1,6 +1,5 @@ import flwr as fl - # Start Flower server fl.server.start_server( server_address="0.0.0.0:8080", diff --git a/examples/quickstart-tensorflow/tfexample/client_app.py b/examples/quickstart-tensorflow/tfexample/client_app.py index f727215eca47..05bf15e074c2 100644 --- a/examples/quickstart-tensorflow/tfexample/client_app.py +++ b/examples/quickstart-tensorflow/tfexample/client_app.py @@ -1,8 +1,7 @@ """tfexample: A Flower / TensorFlow app.""" -from flwr.client import NumPyClient, ClientApp +from flwr.client import ClientApp, NumPyClient from flwr.common import Context - from tfexample.task import load_data, load_model diff --git a/examples/quickstart-tensorflow/tfexample/server_app.py b/examples/quickstart-tensorflow/tfexample/server_app.py index b45cb541f174..053e92588e67 100644 --- a/examples/quickstart-tensorflow/tfexample/server_app.py +++ b/examples/quickstart-tensorflow/tfexample/server_app.py @@ -1,10 +1,10 @@ """tfexample: A Flower / TensorFlow app.""" from typing import List, Tuple -from flwr.common import Context, ndarrays_to_parameters, Metrics + +from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg - from tfexample.task import load_model diff --git a/examples/quickstart-tensorflow/tfexample/task.py b/examples/quickstart-tensorflow/tfexample/task.py index c9c6fa66de27..3d1411821702 100644 --- a/examples/quickstart-tensorflow/tfexample/task.py +++ b/examples/quickstart-tensorflow/tfexample/task.py @@ -3,10 +3,9 @@ import os import keras -from keras import layers from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner - +from keras import layers # Make TensorFlow log less verbose os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" diff --git a/examples/simulation-pytorch/sim.py b/examples/simulation-pytorch/sim.py index dcc0f39a79ef..a435db6d7724 100644 --- a/examples/simulation-pytorch/sim.py +++ b/examples/simulation-pytorch/sim.py @@ -1,19 +1,17 @@ import argparse from collections import OrderedDict -from typing import Dict, Tuple, List - -import torch -from torch.utils.data import DataLoader +from typing import Dict, List, Tuple import flwr as fl -from flwr.common import Metrics -from flwr.common.typing import Scalar - +import torch from datasets import Dataset from datasets.utils.logging import disable_progress_bar +from flwr.common import Metrics +from flwr.common.typing import Scalar from flwr_datasets import FederatedDataset +from torch.utils.data import DataLoader -from utils import Net, train, test, apply_transforms +from utils import Net, apply_transforms, test, train parser = argparse.ArgumentParser(description="Flower Simulation with PyTorch") diff --git a/examples/simulation-pytorch/utils.py b/examples/simulation-pytorch/utils.py index 01f63cc94ba3..702e9886615e 100644 --- a/examples/simulation-pytorch/utils.py +++ b/examples/simulation-pytorch/utils.py @@ -1,8 +1,7 @@ import torch import torch.nn as nn import torch.nn.functional as F - -from torchvision.transforms import ToTensor, Normalize, Compose +from torchvision.transforms import Compose, Normalize, ToTensor # transformation to convert images to tensors and apply normalization diff --git a/examples/simulation-tensorflow/sim.py b/examples/simulation-tensorflow/sim.py index 4014e3c6be72..1ae2db41ab4b 100644 --- a/examples/simulation-tensorflow/sim.py +++ b/examples/simulation-tensorflow/sim.py @@ -1,14 +1,12 @@ -import os import argparse +import os from typing import Dict, List, Tuple -import tensorflow as tf - import flwr as fl +import tensorflow as tf +from datasets import Dataset from flwr.common import Metrics from flwr.simulation.ray_transport.utils import enable_tf_gpu_growth - -from datasets import Dataset from flwr_datasets import FederatedDataset # Make TensorFlow logs less verbose diff --git a/examples/sklearn-logreg-mnist/sklearnexample/client_app.py b/examples/sklearn-logreg-mnist/sklearnexample/client_app.py index ef1dd8618f06..0f0cb8f34e82 100644 --- a/examples/sklearn-logreg-mnist/sklearnexample/client_app.py +++ b/examples/sklearn-logreg-mnist/sklearnexample/client_app.py @@ -4,8 +4,8 @@ from flwr.client import Client, ClientApp, NumPyClient from flwr.common import Context - from sklearn.metrics import log_loss + from sklearnexample.task import ( create_log_reg_and_instantiate_parameters, get_model_parameters, diff --git a/examples/sklearn-logreg-mnist/sklearnexample/server_app.py b/examples/sklearn-logreg-mnist/sklearnexample/server_app.py index 7976cdd08ca9..47f4f5fc19c4 100644 --- a/examples/sklearn-logreg-mnist/sklearnexample/server_app.py +++ b/examples/sklearn-logreg-mnist/sklearnexample/server_app.py @@ -4,9 +4,9 @@ from flwr.server import ServerApp, ServerAppComponents, ServerConfig from flwr.server.strategy import FedAvg from flwr_datasets import FederatedDataset - from sklearn.linear_model import LogisticRegression from sklearn.metrics import log_loss + from sklearnexample.task import ( create_log_reg_and_instantiate_parameters, get_model_parameters, diff --git a/examples/sklearn-logreg-mnist/sklearnexample/task.py b/examples/sklearn-logreg-mnist/sklearnexample/task.py index 38d4203b736c..8e2234f85691 100644 --- a/examples/sklearn-logreg-mnist/sklearnexample/task.py +++ b/examples/sklearn-logreg-mnist/sklearnexample/task.py @@ -1,12 +1,11 @@ """sklearnexample: A Flower / scikit-learn app.""" import numpy as np +from flwr.common import NDArrays from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner from sklearn.linear_model import LogisticRegression -from flwr.common import NDArrays - # This information is needed to create a correct scikit-learn model NUM_UNIQUE_LABELS = 10 # MNIST has 10 classes NUM_FEATURES = 784 # Number of features in MNIST dataset diff --git a/examples/tensorflow-privacy/client.py b/examples/tensorflow-privacy/client.py index 4aec85da014a..85ed8a3d4245 100644 --- a/examples/tensorflow-privacy/client.py +++ b/examples/tensorflow-privacy/client.py @@ -1,10 +1,10 @@ import argparse import os -from flwr.client import ClientApp, NumPyClient + import tensorflow as tf -from flwr_datasets import FederatedDataset import tensorflow_privacy - +from flwr.client import ClientApp, NumPyClient +from flwr_datasets import FederatedDataset from tensorflow_privacy.privacy.analysis.compute_dp_sgd_privacy_lib import ( compute_dp_sgd_privacy_statement, ) diff --git a/examples/tensorflow-privacy/server.py b/examples/tensorflow-privacy/server.py index 1e399fa7e833..5b2ac6a3c4df 100644 --- a/examples/tensorflow-privacy/server.py +++ b/examples/tensorflow-privacy/server.py @@ -1,8 +1,8 @@ from typing import List, Tuple +from flwr.common import Metrics from flwr.server import ServerApp, ServerConfig from flwr.server.strategy import FedAvg -from flwr.common import Metrics def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: diff --git a/examples/vertical-fl/plot.py b/examples/vertical-fl/plot.py index 801f202328c7..3dac7c04a3de 100644 --- a/examples/vertical-fl/plot.py +++ b/examples/vertical-fl/plot.py @@ -1,5 +1,5 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np if __name__ == "__main__": hist = np.load("_static/results/hist.npy", allow_pickle=True).item() diff --git a/examples/vertical-fl/simulation.py b/examples/vertical-fl/simulation.py index f4befc3a073e..1aa1c341d5eb 100644 --- a/examples/vertical-fl/simulation.py +++ b/examples/vertical-fl/simulation.py @@ -1,8 +1,10 @@ +from pathlib import Path + import flwr as fl import numpy as np -from strategy import Strategy + from client import FlowerClient -from pathlib import Path +from strategy import Strategy from task import get_partitions_and_label partitions, label = get_partitions_and_label() diff --git a/examples/vertical-fl/task.py b/examples/vertical-fl/task.py index a3cd415229c5..603a051822e9 100644 --- a/examples/vertical-fl/task.py +++ b/examples/vertical-fl/task.py @@ -1,6 +1,6 @@ -import torch.nn as nn import numpy as np import pandas as pd +import torch.nn as nn def _bin_age(age_series): diff --git a/examples/vit-finetune/client.py b/examples/vit-finetune/client.py index bf91fa0c4328..6226b9363ca4 100644 --- a/examples/vit-finetune/client.py +++ b/examples/vit-finetune/client.py @@ -1,8 +1,8 @@ +import flwr import torch +from flwr.client import NumPyClient from torch.utils.data import DataLoader -import flwr -from flwr.client import NumPyClient from dataset import apply_transforms, get_dataset_with_partitions from model import get_model, set_parameters, train diff --git a/examples/vit-finetune/dataset.py b/examples/vit-finetune/dataset.py index 42e0af560a17..e1e01da61dd4 100644 --- a/examples/vit-finetune/dataset.py +++ b/examples/vit-finetune/dataset.py @@ -1,14 +1,13 @@ +from flwr_datasets import FederatedDataset from torchvision.transforms import ( + CenterCrop, Compose, Normalize, - ToTensor, RandomResizedCrop, Resize, - CenterCrop, + ToTensor, ) -from flwr_datasets import FederatedDataset - def get_dataset_with_partitions(num_partitions: int): """Get Oxford Flowers datasets and partition it. diff --git a/examples/vit-finetune/main.py b/examples/vit-finetune/main.py index c629a6f68980..33ad78a04d47 100644 --- a/examples/vit-finetune/main.py +++ b/examples/vit-finetune/main.py @@ -3,8 +3,8 @@ import flwr as fl import matplotlib.pyplot as plt -from server import strategy from client import client_fn +from server import strategy parser = argparse.ArgumentParser( description="Finetuning of a ViT with Flower Simulation." diff --git a/examples/vit-finetune/model.py b/examples/vit-finetune/model.py index ca7dc1cd9864..a0b8294aa485 100644 --- a/examples/vit-finetune/model.py +++ b/examples/vit-finetune/model.py @@ -1,7 +1,7 @@ from collections import OrderedDict import torch -from torchvision.models import vit_b_16, ViT_B_16_Weights +from torchvision.models import ViT_B_16_Weights, vit_b_16 def get_model(): diff --git a/examples/vit-finetune/server.py b/examples/vit-finetune/server.py index 698bcd45cece..5352d34c4fe2 100644 --- a/examples/vit-finetune/server.py +++ b/examples/vit-finetune/server.py @@ -1,7 +1,7 @@ +import flwr as fl import torch from datasets import Dataset from torch.utils.data import DataLoader -import flwr as fl from dataset import apply_eval_transforms, get_dataset_with_partitions from model import get_model, set_parameters, test diff --git a/examples/whisper-federated-finetuning/centralised.py b/examples/whisper-federated-finetuning/centralised.py index 6af591a7502b..c0e3d60a0697 100644 --- a/examples/whisper-federated-finetuning/centralised.py +++ b/examples/whisper-federated-finetuning/centralised.py @@ -1,19 +1,19 @@ import argparse -from datasets import load_dataset -from transformers import WhisperForConditionalGeneration, WhisperProcessor +import random + +import numpy as np import torch +from datasets import concatenate_datasets, load_dataset from torch.utils.data import DataLoader, WeightedRandomSampler -import numpy as np -from datasets import concatenate_datasets -import random +from transformers import WhisperForConditionalGeneration, WhisperProcessor from utils import ( - get_model, - train_one_epoch, eval_model, - prepare_silences_dataset, get_encoding_fn, + get_model, + prepare_silences_dataset, remove_cols, + train_one_epoch, ) random.seed(1989) diff --git a/examples/whisper-federated-finetuning/client.py b/examples/whisper-federated-finetuning/client.py index d3bb217933f8..d1da5c13ecf8 100644 --- a/examples/whisper-federated-finetuning/client.py +++ b/examples/whisper-federated-finetuning/client.py @@ -1,19 +1,20 @@ import argparse -import torch + import flwr as fl import numpy as np +import torch +from datasets import concatenate_datasets, load_dataset, load_from_disk from torch.utils.data import DataLoader, WeightedRandomSampler -from datasets import load_dataset, load_from_disk, concatenate_datasets from transformers import WhisperProcessor from utils import ( + construct_client_mapping, + get_encoding_fn, get_model, + prepare_silences_dataset, + remove_cols, set_params, train_one_epoch, - remove_cols, - prepare_silences_dataset, - construct_client_mapping, - get_encoding_fn, ) parser = argparse.ArgumentParser(description="Flower+Whisper") diff --git a/examples/whisper-federated-finetuning/server.py b/examples/whisper-federated-finetuning/server.py index 101d43f04ec2..060b162240e5 100644 --- a/examples/whisper-federated-finetuning/server.py +++ b/examples/whisper-federated-finetuning/server.py @@ -1,13 +1,12 @@ import argparse +import flwr as fl import torch from datasets import load_dataset -from transformers import WhisperProcessor from torch.utils.data import DataLoader -import flwr as fl - -from utils import eval_model, get_model, set_params, remove_cols, get_encoding_fn +from transformers import WhisperProcessor +from utils import eval_model, get_encoding_fn, get_model, remove_cols, set_params parser = argparse.ArgumentParser(description="Flower+Whisper") parser.add_argument("--num_rounds", type=int, default=5, help="Number of FL rounds.") diff --git a/examples/whisper-federated-finetuning/sim.py b/examples/whisper-federated-finetuning/sim.py index c04f768bb24a..750a7f705251 100644 --- a/examples/whisper-federated-finetuning/sim.py +++ b/examples/whisper-federated-finetuning/sim.py @@ -1,11 +1,10 @@ import argparse +import flwr as fl import torch from datasets import load_dataset from transformers import WhisperProcessor -import flwr as fl - from client import get_client_fn from server import fit_config, get_evaluate_fn from utils import construct_client_mapping, get_encoding_fn diff --git a/examples/whisper-federated-finetuning/utils.py b/examples/whisper-federated-finetuning/utils.py index 117cf7100ddd..3bae730790a0 100644 --- a/examples/whisper-federated-finetuning/utils.py +++ b/examples/whisper-federated-finetuning/utils.py @@ -1,15 +1,13 @@ -from tqdm import tqdm -import torch import random -from datasets import Dataset -import numpy as np from collections import OrderedDict -from transformers import WhisperForConditionalGeneration - from typing import List import flwr as fl - +import numpy as np +import torch +from datasets import Dataset +from tqdm import tqdm +from transformers import WhisperForConditionalGeneration remove_cols = ["file", "audio", "label", "is_unknown", "speaker_id", "utterance_id"] diff --git a/examples/xgboost-comprehensive/client.py b/examples/xgboost-comprehensive/client.py index 08dd548a386b..879e106493f6 100644 --- a/examples/xgboost-comprehensive/client.py +++ b/examples/xgboost-comprehensive/client.py @@ -2,18 +2,17 @@ from logging import INFO import flwr as fl -from flwr_datasets import FederatedDataset from flwr.common.logger import log +from flwr_datasets import FederatedDataset +from client_utils import XgbClient from dataset import ( instantiate_partitioner, + resplit, train_test_split, transform_dataset_to_dmatrix, - resplit, ) -from utils import client_args_parser, BST_PARAMS, NUM_LOCAL_ROUND -from client_utils import XgbClient - +from utils import BST_PARAMS, NUM_LOCAL_ROUND, client_args_parser warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/xgboost-comprehensive/client_utils.py b/examples/xgboost-comprehensive/client_utils.py index d2e07677ef97..0ef868c505b8 100644 --- a/examples/xgboost-comprehensive/client_utils.py +++ b/examples/xgboost-comprehensive/client_utils.py @@ -1,8 +1,7 @@ from logging import INFO -import xgboost as xgb import flwr as fl -from flwr.common.logger import log +import xgboost as xgb from flwr.common import ( Code, EvaluateIns, @@ -14,6 +13,7 @@ Parameters, Status, ) +from flwr.common.logger import log class XgbClient(fl.client.Client): diff --git a/examples/xgboost-comprehensive/dataset.py b/examples/xgboost-comprehensive/dataset.py index 94959925f833..eebd87219fa6 100644 --- a/examples/xgboost-comprehensive/dataset.py +++ b/examples/xgboost-comprehensive/dataset.py @@ -1,11 +1,12 @@ -import xgboost as xgb from typing import Union + +import xgboost as xgb from datasets import Dataset, DatasetDict, concatenate_datasets from flwr_datasets.partitioner import ( + ExponentialPartitioner, IidPartitioner, LinearPartitioner, SquarePartitioner, - ExponentialPartitioner, ) CORRELATION_TO_PARTITIONER = { diff --git a/examples/xgboost-comprehensive/server.py b/examples/xgboost-comprehensive/server.py index 07dc4bed6db4..1d0dc0aecd43 100644 --- a/examples/xgboost-comprehensive/server.py +++ b/examples/xgboost-comprehensive/server.py @@ -3,19 +3,18 @@ import flwr as fl from flwr.common.logger import log -from flwr_datasets import FederatedDataset from flwr.server.strategy import FedXgbBagging, FedXgbCyclic +from flwr_datasets import FederatedDataset -from utils import server_args_parser +from dataset import resplit, transform_dataset_to_dmatrix from server_utils import ( + CyclicClientManager, eval_config, - fit_config, evaluate_metrics_aggregation, + fit_config, get_evaluate_fn, - CyclicClientManager, ) -from dataset import resplit, transform_dataset_to_dmatrix - +from utils import server_args_parser warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/xgboost-comprehensive/server_utils.py b/examples/xgboost-comprehensive/server_utils.py index 35a31bd9adac..f6610afce5ac 100644 --- a/examples/xgboost-comprehensive/server_utils.py +++ b/examples/xgboost-comprehensive/server_utils.py @@ -1,11 +1,13 @@ -from typing import Dict, List, Optional from logging import INFO +from typing import Dict, List, Optional + import xgboost as xgb -from flwr.common.logger import log from flwr.common import Parameters, Scalar +from flwr.common.logger import log from flwr.server.client_manager import SimpleClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.criterion import Criterion + from utils import BST_PARAMS diff --git a/examples/xgboost-comprehensive/sim.py b/examples/xgboost-comprehensive/sim.py index 09ebbb81fcb4..c29e762370fa 100644 --- a/examples/xgboost-comprehensive/sim.py +++ b/examples/xgboost-comprehensive/sim.py @@ -1,34 +1,29 @@ import warnings from logging import INFO -import xgboost as xgb -from tqdm import tqdm import flwr as fl -from flwr_datasets import FederatedDataset +import xgboost as xgb from flwr.common.logger import log from flwr.server.strategy import FedXgbBagging, FedXgbCyclic +from flwr_datasets import FederatedDataset +from tqdm import tqdm +from client_utils import XgbClient from dataset import ( instantiate_partitioner, + resplit, + separate_xy, train_test_split, transform_dataset_to_dmatrix, - separate_xy, - resplit, -) -from utils import ( - sim_args_parser, - NUM_LOCAL_ROUND, - BST_PARAMS, ) from server_utils import ( + CyclicClientManager, eval_config, - fit_config, evaluate_metrics_aggregation, + fit_config, get_evaluate_fn, - CyclicClientManager, ) -from client_utils import XgbClient - +from utils import BST_PARAMS, NUM_LOCAL_ROUND, sim_args_parser warnings.filterwarnings("ignore", category=UserWarning) diff --git a/examples/xgboost-comprehensive/utils.py b/examples/xgboost-comprehensive/utils.py index abc100da1ade..c3582d803a6a 100644 --- a/examples/xgboost-comprehensive/utils.py +++ b/examples/xgboost-comprehensive/utils.py @@ -1,6 +1,5 @@ import argparse - # Hyper-parameters for xgboost training NUM_LOCAL_ROUND = 1 BST_PARAMS = { diff --git a/examples/xgboost-quickstart/client.py b/examples/xgboost-quickstart/client.py index 5a4d88bb7e43..d505a7ede785 100644 --- a/examples/xgboost-quickstart/client.py +++ b/examples/xgboost-quickstart/client.py @@ -1,13 +1,11 @@ import argparse import warnings -from typing import Union from logging import INFO -from datasets import Dataset, DatasetDict -import xgboost as xgb +from typing import Union import flwr as fl -from flwr_datasets import FederatedDataset -from flwr.common.logger import log +import xgboost as xgb +from datasets import Dataset, DatasetDict from flwr.common import ( Code, EvaluateIns, @@ -19,9 +17,10 @@ Parameters, Status, ) +from flwr.common.logger import log +from flwr_datasets import FederatedDataset from flwr_datasets.partitioner import IidPartitioner - warnings.filterwarnings("ignore", category=UserWarning) # Define arguments parser for the client/partition ID. diff --git a/examples/xgboost-quickstart/server.py b/examples/xgboost-quickstart/server.py index e9239fde696c..2246d32686a4 100644 --- a/examples/xgboost-quickstart/server.py +++ b/examples/xgboost-quickstart/server.py @@ -1,8 +1,8 @@ from typing import Dict + import flwr as fl from flwr.server.strategy import FedXgbBagging - # FL experimental settings pool_size = 2 num_rounds = 5 From 3034810c525e175cfce95fcad70dd4f23fd4934a Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Sun, 11 Aug 2024 20:30:54 +0200 Subject: [PATCH 046/188] refactor(framework:skip) Reorder arguments to improve consistency (#3975) --- src/py/flwr/cli/run/run.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index c5a096293f01..f0a98af3a724 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -109,14 +109,14 @@ def run( raise typer.Exit(code=1) if "address" in federation_config: - _run_with_superexec(federation_config, app, config_overrides) + _run_with_superexec(app, federation_config, config_overrides) else: - _run_without_superexec(app, federation_config, federation, config_overrides) + _run_without_superexec(app, federation_config, config_overrides, federation) def _run_with_superexec( - federation_config: Dict[str, Any], app: Optional[Path], + federation_config: Dict[str, Any], config_overrides: Optional[List[str]], ) -> None: @@ -183,8 +183,8 @@ def on_channel_state_change(channel_connectivity: str) -> None: def _run_without_superexec( app: Optional[Path], federation_config: Dict[str, Any], - federation: str, config_overrides: Optional[List[str]], + federation: str, ) -> None: try: num_supernodes = federation_config["options"]["num-supernodes"] From fa630b053448266960ce43411a0b6a75d92db6b0 Mon Sep 17 00:00:00 2001 From: Steve Laskaridis Date: Mon, 12 Aug 2024 17:37:21 +0100 Subject: [PATCH 047/188] fix(examples:skip) Fix requirements.txt in flowertune example (#3980) --- examples/llm-flowertune/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/llm-flowertune/requirements.txt b/examples/llm-flowertune/requirements.txt index 7c66612eb2a5..2d0e65da3615 100644 --- a/examples/llm-flowertune/requirements.txt +++ b/examples/llm-flowertune/requirements.txt @@ -7,3 +7,4 @@ scipy==1.11.2 peft==0.4.0 fschat[model_worker,webui]==0.2.35 transformers==4.38.1 +hf_transfer==0.1.8 From 01896eb1cbcdf7e59b8050b0f389c8907d53e16f Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Mon, 12 Aug 2024 16:50:38 -0400 Subject: [PATCH 048/188] feat(framework:skip) Add Flower File Storage interface and disk based implementation (#3619) Co-authored-by: Daniel J. Beutel Co-authored-by: Charles Beauville --- src/py/flwr/server/superlink/ffs/__init__.py | 24 +++ src/py/flwr/server/superlink/ffs/disk_ffs.py | 104 +++++++++++++ src/py/flwr/server/superlink/ffs/ffs.py | 79 ++++++++++ src/py/flwr/server/superlink/ffs/ffs_test.py | 150 +++++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 src/py/flwr/server/superlink/ffs/__init__.py create mode 100644 src/py/flwr/server/superlink/ffs/disk_ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs_test.py diff --git a/src/py/flwr/server/superlink/ffs/__init__.py b/src/py/flwr/server/superlink/ffs/__init__.py new file mode 100644 index 000000000000..0273d2a630e1 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower File Storage for large objects.""" + + +from .disk_ffs import DiskFfs as DiskFfs +from .ffs import Ffs as Ffs + +__all__ = [ + "DiskFfs", + "Ffs", +] diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py new file mode 100644 index 000000000000..5331af500464 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -0,0 +1,104 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Disk based Flower File Storage.""" + +import hashlib +import json +from pathlib import Path +from typing import Dict, List, Tuple + +from flwr.server.superlink.ffs.ffs import Ffs + + +class DiskFfs(Ffs): # pylint: disable=R0904 + """Disk-based Flower File Storage interface for large objects.""" + + def __init__(self, base_dir: str) -> None: + """Create a new DiskFfs instance. + + Parameters + ---------- + base_dir : str + The base directory to store the objects. + """ + self.base_dir = Path(base_dir) + + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata and return key (hash of content). + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + key : str + The key (sha256hex hash) of the content. + """ + content_hash = hashlib.sha256(content).hexdigest() + + self.base_dir.mkdir(exist_ok=True, parents=True) + (self.base_dir / content_hash).write_bytes(content) + (self.base_dir / f"{content_hash}.META").write_text(json.dumps(meta)) + + return content_hash + + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object content and metadata. + + Parameters + ---------- + key : str + The sha256hex hash of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object content and metadata. + """ + content = (self.base_dir / key).read_bytes() + meta = json.loads((self.base_dir / f"{key}.META").read_text()) + + return content, meta + + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + key : str + The sha256hex hash of the object to be deleted. + """ + (self.base_dir / key).unlink() + (self.base_dir / f"{key}.META").unlink() + + def list(self) -> List[str]: + """List all keys. + + Return all available keys in this `Ffs` instance. + This can be combined with, for example, + the `delete` method to delete objects. + + Returns + ------- + List[str] + A list of all available keys. + """ + return [ + item.name for item in self.base_dir.iterdir() if not item.suffix == ".META" + ] diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py new file mode 100644 index 000000000000..622988141c9d --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -0,0 +1,79 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Abstract base class for Flower File Storage interface.""" + + +import abc +from typing import Dict, List, Tuple + + +class Ffs(abc.ABC): # pylint: disable=R0904 + """Abstract Flower File Storage interface for large objects.""" + + @abc.abstractmethod + def put(self, content: bytes, meta: Dict[str, str]) -> str: + """Store bytes and metadata and return sha256hex hash of data as str. + + Parameters + ---------- + content : bytes + The content to be stored. + meta : Dict[str, str] + The metadata to be stored. + + Returns + ------- + key : str + The key (sha256hex hash) of the content. + """ + + @abc.abstractmethod + def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + """Return tuple containing the object content and metadata. + + Parameters + ---------- + key : str + The key (sha256hex hash) of the object to be retrieved. + + Returns + ------- + Tuple[bytes, Dict[str, str]] + A tuple containing the object content and metadata. + """ + + @abc.abstractmethod + def delete(self, key: str) -> None: + """Delete object with hash. + + Parameters + ---------- + key : str + The key (sha256hex hash) of the object to be deleted. + """ + + @abc.abstractmethod + def list(self) -> List[str]: + """List keys of all stored objects. + + Return all available keys in this `Ffs` instance. + This can be combined with, for example, + the `delete` method to delete objects. + + Returns + ------- + List[str] + A list of all available keys. + """ diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py new file mode 100644 index 000000000000..3b25ac7b206a --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -0,0 +1,150 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests all Ffs implemenations have to conform to.""" +# pylint: disable=invalid-name, disable=R0904 + +import hashlib +import json +import os +import tempfile +import unittest +from abc import abstractmethod +from typing import Dict + +from flwr.server.superlink.ffs import DiskFfs, Ffs + + +class FfsTest(unittest.TestCase): + """Test all ffs implementations.""" + + # This is to True in each child class + __test__ = False + + tmp_dir: tempfile.TemporaryDirectory # type: ignore + + @abstractmethod + def ffs_factory(self) -> Ffs: + """Provide Ffs implementation to test.""" + raise NotImplementedError() + + def test_put(self) -> None: + """Test if object can be stored.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content = b"content" + hash_expected = hashlib.sha256(content).hexdigest() + + # Execute + hash_actual = ffs.put(b"content", {"meta": "data"}) + + # Assert + assert isinstance(hash_actual, str) + assert len(hash_actual) == 64 + assert hash_actual == hash_expected + + # Check if file was created + assert {hash_expected, f"{hash_expected}.META"} == set( + os.listdir(self.tmp_dir.name) + ) + + def test_get(self) -> None: + """Test if object can be retrieved.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", + ) as file: + json.dump(meta_expected, file) + + # Execute + content_actual, meta_actual = ffs.get(hash_expected) + + # Assert + assert content_actual == content_expected + assert meta_actual == meta_expected + + def test_delete(self) -> None: + """Test if object can be deleted.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", + ) as file: + json.dump(meta_expected, file) + + # Execute + ffs.delete(hash_expected) + + # Assert + assert set() == set(os.listdir(self.tmp_dir.name)) + + def test_list(self) -> None: + """Test if object hashes can be listed.""" + # Prepare + ffs: Ffs = self.ffs_factory() + content_expected = b"content" + hash_expected = hashlib.sha256(content_expected).hexdigest() + meta_expected: Dict[str, str] = {"meta_key": "meta_value"} + + with open(os.path.join(self.tmp_dir.name, hash_expected), "wb") as file: + file.write(content_expected) + + with open( + os.path.join(self.tmp_dir.name, f"{hash_expected}.META"), + "w", + encoding="utf-8", + ) as file: + json.dump(meta_expected, file) + + # Execute + hashes = ffs.list() + + # Assert + assert {hash_expected} == set(hashes) + + +class DiskFfsTest(FfsTest, unittest.TestCase): + """Test DiskFfs implementation.""" + + __test__ = True + + def ffs_factory(self) -> DiskFfs: + """Return SqliteState with file-based database.""" + # pylint: disable-next=consider-using-with,attribute-defined-outside-init + self.tmp_dir = tempfile.TemporaryDirectory() + ffs = DiskFfs(self.tmp_dir.name) + return ffs + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 59ad1368be88b306fd9bfe442b729e8a8ad4836e Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Mon, 12 Aug 2024 22:17:40 +0100 Subject: [PATCH 049/188] feat(framework) Add `ClientAppIo` proto (#3970) Co-authored-by: Daniel J. Beutel Co-authored-by: jafermarq Co-authored-by: Charles Beauville Co-authored-by: Taner Topal --- src/proto/flwr/proto/clientappio.proto | 40 +++++++ src/proto/flwr/proto/message.proto | 46 ++++++++ src/py/flwr/proto/clientappio_pb2.py | 41 +++++++ src/py/flwr/proto/clientappio_pb2.pyi | 110 +++++++++++++++++++ src/py/flwr/proto/clientappio_pb2_grpc.py | 101 +++++++++++++++++ src/py/flwr/proto/clientappio_pb2_grpc.pyi | 40 +++++++ src/py/flwr/proto/message_pb2.py | 41 +++++++ src/py/flwr/proto/message_pb2.pyi | 122 +++++++++++++++++++++ src/py/flwr/proto/message_pb2_grpc.py | 4 + src/py/flwr/proto/message_pb2_grpc.pyi | 4 + src/py/flwr_tool/protoc_test.py | 2 +- 11 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 src/proto/flwr/proto/clientappio.proto create mode 100644 src/proto/flwr/proto/message.proto create mode 100644 src/py/flwr/proto/clientappio_pb2.py create mode 100644 src/py/flwr/proto/clientappio_pb2.pyi create mode 100644 src/py/flwr/proto/clientappio_pb2_grpc.py create mode 100644 src/py/flwr/proto/clientappio_pb2_grpc.pyi create mode 100644 src/py/flwr/proto/message_pb2.py create mode 100644 src/py/flwr/proto/message_pb2.pyi create mode 100644 src/py/flwr/proto/message_pb2_grpc.py create mode 100644 src/py/flwr/proto/message_pb2_grpc.pyi diff --git a/src/proto/flwr/proto/clientappio.proto b/src/proto/flwr/proto/clientappio.proto new file mode 100644 index 000000000000..d73ed086f40d --- /dev/null +++ b/src/proto/flwr/proto/clientappio.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package flwr.proto; + +import "flwr/proto/fab.proto"; +import "flwr/proto/run.proto"; +import "flwr/proto/message.proto"; + +service ClientAppIo { + // Get Message, Context, and Run + rpc PullClientAppInputs(PullClientAppInputsRequest) + returns (PullClientAppInputsResponse) {} + + // Send updated Message and Context + rpc PushClientAppOutputs(PushClientAppOutputsRequest) + returns (PushClientAppOutputsResponse) {} +} + +enum ClientAppOutputCode { + SUCCESS = 0; + DEADLINE_EXCEEDED = 1; + UNKNOWN_ERROR = 2; +} +message ClientAppOutputStatus { + ClientAppOutputCode code = 1; + string message = 2; +} + +message PullClientAppInputsRequest { sint64 token = 1; } +message PullClientAppInputsResponse { + Message message = 1; + Context context = 2; + Run run = 3; +} +message PushClientAppOutputsRequest { + sint64 token = 1; + Message message = 2; + Context context = 3; +} +message PushClientAppOutputsResponse { ClientAppOutputStatus status = 1; } diff --git a/src/proto/flwr/proto/message.proto b/src/proto/flwr/proto/message.proto new file mode 100644 index 000000000000..d568522e761e --- /dev/null +++ b/src/proto/flwr/proto/message.proto @@ -0,0 +1,46 @@ +// Copyright 2024 Flower Labs GmbH. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ============================================================================== + +syntax = "proto3"; + +package flwr.proto; + +import "flwr/proto/error.proto"; +import "flwr/proto/recordset.proto"; +import "flwr/proto/transport.proto"; + +message Message { + Metadata metadata = 1; + RecordSet content = 2; + Error error = 3; +} + +message Context { + sint64 node_id = 1; + map node_config = 2; + RecordSet state = 3; + map run_config = 4; +} + +message Metadata { + sint64 run_id = 1; + string message_id = 2; + sint64 src_node_id = 3; + sint64 dst_node_id = 4; + string reply_to_message = 5; + string group_id = 6; + double ttl = 7; + string message_type = 8; +} diff --git a/src/py/flwr/proto/clientappio_pb2.py b/src/py/flwr/proto/clientappio_pb2.py new file mode 100644 index 000000000000..2234e3c2a8af --- /dev/null +++ b/src/py/flwr/proto/clientappio_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: flwr/proto/clientappio.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 +from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2 +from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\"\x87\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xe4\x01\n\x0b\x43lientAppIo\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.clientappio_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=591 + _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=667 + _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_start=114 + _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_end=201 + _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=203 + _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=246 + _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=249 + _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=384 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=386 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=506 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=508 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=589 + _globals['_CLIENTAPPIO']._serialized_start=670 + _globals['_CLIENTAPPIO']._serialized_end=898 +# @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/clientappio_pb2.pyi b/src/py/flwr/proto/clientappio_pb2.pyi new file mode 100644 index 000000000000..31c9dc4c6d14 --- /dev/null +++ b/src/py/flwr/proto/clientappio_pb2.pyi @@ -0,0 +1,110 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import flwr.proto.message_pb2 +import flwr.proto.run_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _ClientAppOutputCode: + ValueType = typing.NewType('ValueType', builtins.int) + V: typing_extensions.TypeAlias = ValueType +class _ClientAppOutputCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ClientAppOutputCode.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + SUCCESS: _ClientAppOutputCode.ValueType # 0 + DEADLINE_EXCEEDED: _ClientAppOutputCode.ValueType # 1 + UNKNOWN_ERROR: _ClientAppOutputCode.ValueType # 2 +class ClientAppOutputCode(_ClientAppOutputCode, metaclass=_ClientAppOutputCodeEnumTypeWrapper): + pass + +SUCCESS: ClientAppOutputCode.ValueType # 0 +DEADLINE_EXCEEDED: ClientAppOutputCode.ValueType # 1 +UNKNOWN_ERROR: ClientAppOutputCode.ValueType # 2 +global___ClientAppOutputCode = ClientAppOutputCode + + +class ClientAppOutputStatus(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + CODE_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + code: global___ClientAppOutputCode.ValueType + message: typing.Text + def __init__(self, + *, + code: global___ClientAppOutputCode.ValueType = ..., + message: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ... +global___ClientAppOutputStatus = ClientAppOutputStatus + +class PullClientAppInputsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TOKEN_FIELD_NUMBER: builtins.int + token: builtins.int + def __init__(self, + *, + token: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["token",b"token"]) -> None: ... +global___PullClientAppInputsRequest = PullClientAppInputsRequest + +class PullClientAppInputsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + MESSAGE_FIELD_NUMBER: builtins.int + CONTEXT_FIELD_NUMBER: builtins.int + RUN_FIELD_NUMBER: builtins.int + @property + def message(self) -> flwr.proto.message_pb2.Message: ... + @property + def context(self) -> flwr.proto.message_pb2.Context: ... + @property + def run(self) -> flwr.proto.run_pb2.Run: ... + def __init__(self, + *, + message: typing.Optional[flwr.proto.message_pb2.Message] = ..., + context: typing.Optional[flwr.proto.message_pb2.Context] = ..., + run: typing.Optional[flwr.proto.run_pb2.Run] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message","run",b"run"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message","run",b"run"]) -> None: ... +global___PullClientAppInputsResponse = PullClientAppInputsResponse + +class PushClientAppOutputsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TOKEN_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + CONTEXT_FIELD_NUMBER: builtins.int + token: builtins.int + @property + def message(self) -> flwr.proto.message_pb2.Message: ... + @property + def context(self) -> flwr.proto.message_pb2.Context: ... + def __init__(self, + *, + token: builtins.int = ..., + message: typing.Optional[flwr.proto.message_pb2.Message] = ..., + context: typing.Optional[flwr.proto.message_pb2.Context] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message","token",b"token"]) -> None: ... +global___PushClientAppOutputsRequest = PushClientAppOutputsRequest + +class PushClientAppOutputsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + STATUS_FIELD_NUMBER: builtins.int + @property + def status(self) -> global___ClientAppOutputStatus: ... + def __init__(self, + *, + status: typing.Optional[global___ClientAppOutputStatus] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["status",b"status"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["status",b"status"]) -> None: ... +global___PushClientAppOutputsResponse = PushClientAppOutputsResponse diff --git a/src/py/flwr/proto/clientappio_pb2_grpc.py b/src/py/flwr/proto/clientappio_pb2_grpc.py new file mode 100644 index 000000000000..b244ef4a5b1d --- /dev/null +++ b/src/py/flwr/proto/clientappio_pb2_grpc.py @@ -0,0 +1,101 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from flwr.proto import clientappio_pb2 as flwr_dot_proto_dot_clientappio__pb2 + + +class ClientAppIoStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.PullClientAppInputs = channel.unary_unary( + '/flwr.proto.ClientAppIo/PullClientAppInputs', + request_serializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsRequest.SerializeToString, + response_deserializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsResponse.FromString, + ) + self.PushClientAppOutputs = channel.unary_unary( + '/flwr.proto.ClientAppIo/PushClientAppOutputs', + request_serializer=flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsRequest.SerializeToString, + response_deserializer=flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsResponse.FromString, + ) + + +class ClientAppIoServicer(object): + """Missing associated documentation comment in .proto file.""" + + def PullClientAppInputs(self, request, context): + """Get Message, Context, and Run + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def PushClientAppOutputs(self, request, context): + """Send updated Message and Context + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ClientAppIoServicer_to_server(servicer, server): + rpc_method_handlers = { + 'PullClientAppInputs': grpc.unary_unary_rpc_method_handler( + servicer.PullClientAppInputs, + request_deserializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsRequest.FromString, + response_serializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsResponse.SerializeToString, + ), + 'PushClientAppOutputs': grpc.unary_unary_rpc_method_handler( + servicer.PushClientAppOutputs, + request_deserializer=flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsRequest.FromString, + response_serializer=flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'flwr.proto.ClientAppIo', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class ClientAppIo(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def PullClientAppInputs(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/PullClientAppInputs', + flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsRequest.SerializeToString, + flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def PushClientAppOutputs(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/PushClientAppOutputs', + flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsRequest.SerializeToString, + flwr_dot_proto_dot_clientappio__pb2.PushClientAppOutputsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/py/flwr/proto/clientappio_pb2_grpc.pyi b/src/py/flwr/proto/clientappio_pb2_grpc.pyi new file mode 100644 index 000000000000..4503e11f17ae --- /dev/null +++ b/src/py/flwr/proto/clientappio_pb2_grpc.pyi @@ -0,0 +1,40 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import abc +import flwr.proto.clientappio_pb2 +import grpc + +class ClientAppIoStub: + def __init__(self, channel: grpc.Channel) -> None: ... + PullClientAppInputs: grpc.UnaryUnaryMultiCallable[ + flwr.proto.clientappio_pb2.PullClientAppInputsRequest, + flwr.proto.clientappio_pb2.PullClientAppInputsResponse] + """Get Message, Context, and Run""" + + PushClientAppOutputs: grpc.UnaryUnaryMultiCallable[ + flwr.proto.clientappio_pb2.PushClientAppOutputsRequest, + flwr.proto.clientappio_pb2.PushClientAppOutputsResponse] + """Send updated Message and Context""" + + +class ClientAppIoServicer(metaclass=abc.ABCMeta): + @abc.abstractmethod + def PullClientAppInputs(self, + request: flwr.proto.clientappio_pb2.PullClientAppInputsRequest, + context: grpc.ServicerContext, + ) -> flwr.proto.clientappio_pb2.PullClientAppInputsResponse: + """Get Message, Context, and Run""" + pass + + @abc.abstractmethod + def PushClientAppOutputs(self, + request: flwr.proto.clientappio_pb2.PushClientAppOutputsRequest, + context: grpc.ServicerContext, + ) -> flwr.proto.clientappio_pb2.PushClientAppOutputsResponse: + """Send updated Message and Context""" + pass + + +def add_ClientAppIoServicer_to_server(servicer: ClientAppIoServicer, server: grpc.Server) -> None: ... diff --git a/src/py/flwr/proto/message_pb2.py b/src/py/flwr/proto/message_pb2.py new file mode 100644 index 000000000000..1dfa5656ea79 --- /dev/null +++ b/src/py/flwr/proto/message_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: flwr/proto/message.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from flwr.proto import error_pb2 as flwr_dot_proto_dot_error__pb2 +from flwr.proto import recordset_pb2 as flwr_dot_proto_dot_recordset__pb2 +from flwr.proto import transport_pb2 as flwr_dot_proto_dot_transport__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/message.proto\x12\nflwr.proto\x1a\x16\x66lwr/proto/error.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x1a\x66lwr/proto/transport.proto\"{\n\x07Message\x12&\n\x08metadata\x18\x01 \x01(\x0b\x32\x14.flwr.proto.Metadata\x12&\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12 \n\x05\x65rror\x18\x03 \x01(\x0b\x32\x11.flwr.proto.Error\"\xbf\x02\n\x07\x43ontext\x12\x0f\n\x07node_id\x18\x01 \x01(\x12\x12\x38\n\x0bnode_config\x18\x02 \x03(\x0b\x32#.flwr.proto.Context.NodeConfigEntry\x12$\n\x05state\x18\x03 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12\x36\n\nrun_config\x18\x04 \x03(\x0b\x32\".flwr.proto.Context.RunConfigEntry\x1a\x45\n\x0fNodeConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\x1a\x44\n\x0eRunConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\xa7\x01\n\x08Metadata\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x12\x12\n\nmessage_id\x18\x02 \x01(\t\x12\x13\n\x0bsrc_node_id\x18\x03 \x01(\x12\x12\x13\n\x0b\x64st_node_id\x18\x04 \x01(\x12\x12\x18\n\x10reply_to_message\x18\x05 \x01(\t\x12\x10\n\x08group_id\x18\x06 \x01(\t\x12\x0b\n\x03ttl\x18\x07 \x01(\x01\x12\x14\n\x0cmessage_type\x18\x08 \x01(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.message_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_CONTEXT_NODECONFIGENTRY']._options = None + _globals['_CONTEXT_NODECONFIGENTRY']._serialized_options = b'8\001' + _globals['_CONTEXT_RUNCONFIGENTRY']._options = None + _globals['_CONTEXT_RUNCONFIGENTRY']._serialized_options = b'8\001' + _globals['_MESSAGE']._serialized_start=120 + _globals['_MESSAGE']._serialized_end=243 + _globals['_CONTEXT']._serialized_start=246 + _globals['_CONTEXT']._serialized_end=565 + _globals['_CONTEXT_NODECONFIGENTRY']._serialized_start=426 + _globals['_CONTEXT_NODECONFIGENTRY']._serialized_end=495 + _globals['_CONTEXT_RUNCONFIGENTRY']._serialized_start=497 + _globals['_CONTEXT_RUNCONFIGENTRY']._serialized_end=565 + _globals['_METADATA']._serialized_start=568 + _globals['_METADATA']._serialized_end=735 +# @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/message_pb2.pyi b/src/py/flwr/proto/message_pb2.pyi new file mode 100644 index 000000000000..68b98430a59a --- /dev/null +++ b/src/py/flwr/proto/message_pb2.pyi @@ -0,0 +1,122 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import flwr.proto.error_pb2 +import flwr.proto.recordset_pb2 +import flwr.proto.transport_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class Message(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + METADATA_FIELD_NUMBER: builtins.int + CONTENT_FIELD_NUMBER: builtins.int + ERROR_FIELD_NUMBER: builtins.int + @property + def metadata(self) -> global___Metadata: ... + @property + def content(self) -> flwr.proto.recordset_pb2.RecordSet: ... + @property + def error(self) -> flwr.proto.error_pb2.Error: ... + def __init__(self, + *, + metadata: typing.Optional[global___Metadata] = ..., + content: typing.Optional[flwr.proto.recordset_pb2.RecordSet] = ..., + error: typing.Optional[flwr.proto.error_pb2.Error] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["content",b"content","error",b"error","metadata",b"metadata"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["content",b"content","error",b"error","metadata",b"metadata"]) -> None: ... +global___Message = Message + +class Context(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class NodeConfigEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: typing.Text + @property + def value(self) -> flwr.proto.transport_pb2.Scalar: ... + def __init__(self, + *, + key: typing.Text = ..., + value: typing.Optional[flwr.proto.transport_pb2.Scalar] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... + + class RunConfigEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: typing.Text + @property + def value(self) -> flwr.proto.transport_pb2.Scalar: ... + def __init__(self, + *, + key: typing.Text = ..., + value: typing.Optional[flwr.proto.transport_pb2.Scalar] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value",b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... + + NODE_ID_FIELD_NUMBER: builtins.int + NODE_CONFIG_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + RUN_CONFIG_FIELD_NUMBER: builtins.int + node_id: builtins.int + @property + def node_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... + @property + def state(self) -> flwr.proto.recordset_pb2.RecordSet: ... + @property + def run_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... + def __init__(self, + *, + node_id: builtins.int = ..., + node_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., + state: typing.Optional[flwr.proto.recordset_pb2.RecordSet] = ..., + run_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["state",b"state"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["node_config",b"node_config","node_id",b"node_id","run_config",b"run_config","state",b"state"]) -> None: ... +global___Context = Context + +class Metadata(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + RUN_ID_FIELD_NUMBER: builtins.int + MESSAGE_ID_FIELD_NUMBER: builtins.int + SRC_NODE_ID_FIELD_NUMBER: builtins.int + DST_NODE_ID_FIELD_NUMBER: builtins.int + REPLY_TO_MESSAGE_FIELD_NUMBER: builtins.int + GROUP_ID_FIELD_NUMBER: builtins.int + TTL_FIELD_NUMBER: builtins.int + MESSAGE_TYPE_FIELD_NUMBER: builtins.int + run_id: builtins.int + message_id: typing.Text + src_node_id: builtins.int + dst_node_id: builtins.int + reply_to_message: typing.Text + group_id: typing.Text + ttl: builtins.float + message_type: typing.Text + def __init__(self, + *, + run_id: builtins.int = ..., + message_id: typing.Text = ..., + src_node_id: builtins.int = ..., + dst_node_id: builtins.int = ..., + reply_to_message: typing.Text = ..., + group_id: typing.Text = ..., + ttl: builtins.float = ..., + message_type: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["dst_node_id",b"dst_node_id","group_id",b"group_id","message_id",b"message_id","message_type",b"message_type","reply_to_message",b"reply_to_message","run_id",b"run_id","src_node_id",b"src_node_id","ttl",b"ttl"]) -> None: ... +global___Metadata = Metadata diff --git a/src/py/flwr/proto/message_pb2_grpc.py b/src/py/flwr/proto/message_pb2_grpc.py new file mode 100644 index 000000000000..2daafffebfc8 --- /dev/null +++ b/src/py/flwr/proto/message_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/src/py/flwr/proto/message_pb2_grpc.pyi b/src/py/flwr/proto/message_pb2_grpc.pyi new file mode 100644 index 000000000000..f3a5a087ef5d --- /dev/null +++ b/src/py/flwr/proto/message_pb2_grpc.pyi @@ -0,0 +1,4 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" diff --git a/src/py/flwr_tool/protoc_test.py b/src/py/flwr_tool/protoc_test.py index 6ba7daa0efea..6f9127304f25 100644 --- a/src/py/flwr_tool/protoc_test.py +++ b/src/py/flwr_tool/protoc_test.py @@ -28,4 +28,4 @@ def test_directories() -> None: def test_proto_file_count() -> None: """Test if the correct number of proto files were captured by the glob.""" - assert len(PROTO_FILES) == 11 + assert len(PROTO_FILES) == 13 From 1100d896d59147bbb1c8886c135b244fa1dc6d4d Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Mon, 12 Aug 2024 22:26:58 +0100 Subject: [PATCH 050/188] feat(framework) Add internal `flwr-clientapp` command (#3973) Co-authored-by: Daniel J. Beutel --- pyproject.toml | 1 + src/py/flwr/client/supernode/__init__.py | 2 ++ src/py/flwr/client/supernode/app.py | 25 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7d72f8caf32d..1369ee4e968a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ flower-supernode = "flwr.client:run_supernode" flower-client-app = "flwr.client:run_client_app" flower-server-app = "flwr.server:run_server_app" flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" +flwr-clientapp = "flwr.client.supernode:flwr_clientapp" [tool.poetry.dependencies] python = "^3.8" diff --git a/src/py/flwr/client/supernode/__init__.py b/src/py/flwr/client/supernode/__init__.py index bc505f693875..128d0286d625 100644 --- a/src/py/flwr/client/supernode/__init__.py +++ b/src/py/flwr/client/supernode/__init__.py @@ -15,10 +15,12 @@ """Flower SuperNode.""" +from .app import flwr_clientapp as flwr_clientapp from .app import run_client_app as run_client_app from .app import run_supernode as run_supernode __all__ = [ + "flwr_clientapp", "run_client_app", "run_supernode", ] diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 861ccbe34ece..5840b57c0ab6 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -120,6 +120,31 @@ def run_client_app() -> None: register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE) +def flwr_clientapp() -> None: + """Run process-isolated Flower ClientApp.""" + log(INFO, "Starting Flower ClientApp") + + parser = argparse.ArgumentParser( + description="Run a Flower ClientApp", + ) + parser.add_argument( + "--address", + help="Address of SuperNode ClientAppIo gRPC servicer", + ) + parser.add_argument( + "--token", + help="Unique token generated by SuperNode for each ClientApp execution", + ) + args = parser.parse_args() + log( + DEBUG, + "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s " + "with the token %s", + args.address, + args.token, + ) + + def _warn_deprecated_server_arg(args: argparse.Namespace) -> None: """Warn about the deprecated argument `--server`.""" if args.server != ADDRESS_FLEET_API_GRPC_RERE: From 3c11747576e2343f0762f9e24927db63d1a69388 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Tue, 13 Aug 2024 10:08:27 +0200 Subject: [PATCH 051/188] fix(*:skip) Fix Dependabot PR title check (#3996) Signed-off-by: Robert Steiner Co-authored-by: Taner Topal --- dev/check_pr_title.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/check_pr_title.py b/dev/check_pr_title.py index 7dbd17394148..b4fcccafc6f5 100644 --- a/dev/check_pr_title.py +++ b/dev/check_pr_title.py @@ -46,7 +46,7 @@ error = "it doesn't have the correct format" # This check is there to ignore dependabot PRs from title checks - if pr_title.startswith("chore"): + if pr_title.startswith("build"): sys.exit(0) elif not match: valid = False From cd5a0728688197e1a2450e34873c72b8bf03b9e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:51:52 +0200 Subject: [PATCH 052/188] build(deps): bump docker/setup-qemu-action from 3.1.0 to 3.2.0 (#3939) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/5927c834f5b4fdf503fca6f4c7eccda82949e1ee...49b3bc8e6bdd4a60e6116a5414239cba5943d3cf) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/_docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index 7d78ea881034..b766087f80a9 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -81,7 +81,7 @@ jobs: - name: Set up QEMU if: matrix.platform.qemu != '' - uses: docker/setup-qemu-action@5927c834f5b4fdf503fca6f4c7eccda82949e1ee # v3.1.0 + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 with: platforms: ${{ matrix.platform.qemu }} From ebc050ed04ad32adff186fcbbdbfc3854f3d9802 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:15:05 +0200 Subject: [PATCH 053/188] build(deps): bump docker/login-action from 3.2.0 to 3.3.0 (#3940) Bumps [docker/login-action](https://github.com/docker/login-action) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/0d4c9c5ea7693da7b068278f7b52bda2a190a446...9780b0c442fbb1117ed29e0efdff1e18412f7567) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Robert Steiner --- .github/workflows/_docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index b766087f80a9..d9a0edf2309e 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -95,7 +95,7 @@ jobs: uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0 - name: Login to Docker Hub - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: username: ${{ secrets.dockerhub-user }} password: ${{ secrets.dockerhub-token }} @@ -155,7 +155,7 @@ jobs: uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0 - name: Login to Docker Hub - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: username: ${{ secrets.dockerhub-user }} password: ${{ secrets.dockerhub-token }} From 8978b23c7ba14215781a8604ddf46c28e47cb55b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:48:28 +0200 Subject: [PATCH 054/188] build(deps): bump docker/setup-buildx-action from 3.4.0 to 3.6.1 (#3955) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.4.0 to 3.6.1. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/4fd812986e6c8c2a69e18311145f9371337f27d4...988b5a0280414f521da01fcc63a27aeeb4b104db) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Robert Steiner --- .github/workflows/_docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index d9a0edf2309e..e1faa116f3f3 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -92,7 +92,7 @@ jobs: images: ${{ inputs.namespace-repository }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -152,7 +152,7 @@ jobs: tags: ${{ inputs.tags }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 From e85399d9a7531c9a754065c69040c8374a31804a Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Tue, 13 Aug 2024 13:02:59 +0200 Subject: [PATCH 055/188] feat(framework) Add `get_fab` func to connection (#3946) --- src/py/flwr/client/app.py | 5 +++-- src/py/flwr/client/grpc_adapter_client/connection.py | 4 +++- src/py/flwr/client/grpc_client/connection.py | 5 +++-- src/py/flwr/client/grpc_client/connection_test.py | 2 +- .../client/grpc_rere_client/client_interceptor_test.py | 10 +++++----- src/py/flwr/client/grpc_rere_client/connection.py | 9 +++++++-- src/py/flwr/client/rest_client/connection.py | 9 +++++++-- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 526f26cb8cc3..7acd75353821 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -42,7 +42,7 @@ from flwr.common.logger import log, warn_deprecated_feature from flwr.common.message import Error from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential -from flwr.common.typing import Run, UserConfig +from flwr.common.typing import Fab, Run, UserConfig from .grpc_adapter_client.connection import grpc_adapter from .grpc_client.connection import grpc_connection @@ -333,7 +333,7 @@ def _on_backoff(retry_state: RetryState) -> None: root_certificates, authentication_keys, ) as conn: - receive, send, create_node, delete_node, get_run = conn + receive, send, create_node, delete_node, get_run, _ = conn # Register node when connecting the first time if node_state is None: @@ -606,6 +606,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[ Optional[Callable[[], Optional[int]]], Optional[Callable[[], None]], Optional[Callable[[int], Run]], + Optional[Callable[[str], Fab]], ] ], ], diff --git a/src/py/flwr/client/grpc_adapter_client/connection.py b/src/py/flwr/client/grpc_adapter_client/connection.py index 80a5cf0b4656..f9f7b1043524 100644 --- a/src/py/flwr/client/grpc_adapter_client/connection.py +++ b/src/py/flwr/client/grpc_adapter_client/connection.py @@ -27,7 +27,7 @@ from flwr.common.logger import log from flwr.common.message import Message from flwr.common.retry_invoker import RetryInvoker -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run @contextmanager @@ -47,6 +47,7 @@ def grpc_adapter( # pylint: disable=R0913 Optional[Callable[[], Optional[int]]], Optional[Callable[[], None]], Optional[Callable[[int], Run]], + Optional[Callable[[str], Fab]], ] ]: """Primitives for request/response-based interaction with a server via GrpcAdapter. @@ -80,6 +81,7 @@ def grpc_adapter( # pylint: disable=R0913 create_node : Optional[Callable] delete_node : Optional[Callable] get_run : Optional[Callable] + get_fab : Optional[Callable] """ if authentication_keys is not None: log(ERROR, "Client authentication is not supported for this transport type.") diff --git a/src/py/flwr/client/grpc_client/connection.py b/src/py/flwr/client/grpc_client/connection.py index a6417106d51b..489891f55436 100644 --- a/src/py/flwr/client/grpc_client/connection.py +++ b/src/py/flwr/client/grpc_client/connection.py @@ -38,7 +38,7 @@ from flwr.common.grpc import create_channel from flwr.common.logger import log from flwr.common.retry_invoker import RetryInvoker -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run from flwr.proto.transport_pb2 import ( # pylint: disable=E0611 ClientMessage, Reason, @@ -75,6 +75,7 @@ def grpc_connection( # pylint: disable=R0913, R0915 Optional[Callable[[], Optional[int]]], Optional[Callable[[], None]], Optional[Callable[[int], Run]], + Optional[Callable[[str], Fab]], ] ]: """Establish a gRPC connection to a gRPC server. @@ -235,7 +236,7 @@ def send(message: Message) -> None: try: # Yield methods - yield (receive, send, None, None, None) + yield (receive, send, None, None, None, None) finally: # Make sure to have a final channel.close() diff --git a/src/py/flwr/client/grpc_client/connection_test.py b/src/py/flwr/client/grpc_client/connection_test.py index da7800b26639..bd377ef3470a 100644 --- a/src/py/flwr/client/grpc_client/connection_test.py +++ b/src/py/flwr/client/grpc_client/connection_test.py @@ -138,7 +138,7 @@ def run_client() -> int: max_time=None, ), ) as conn: - receive, send, _, _, _ = conn + receive, send, _, _, _, _ = conn # Setup processing loop while True: diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py index 9607dac5679e..f5d3a6d2b6f9 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py @@ -202,7 +202,7 @@ def test_client_auth_create_node(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, _, create_node, _, _ = conn + _, _, create_node, _, _, _ = conn assert create_node is not None create_node() expected_client_metadata = ( @@ -227,7 +227,7 @@ def test_client_auth_delete_node(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, _, _, delete_node, _ = conn + _, _, _, delete_node, _, _ = conn assert delete_node is not None delete_node() shared_secret = generate_shared_key( @@ -266,7 +266,7 @@ def test_client_auth_receive(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - receive, _, _, _, _ = conn + receive, _, _, _, _, _ = conn assert receive is not None receive() shared_secret = generate_shared_key( @@ -306,7 +306,7 @@ def test_client_auth_send(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, send, _, _, _ = conn + _, send, _, _, _, _ = conn assert send is not None send(message) shared_secret = generate_shared_key( @@ -345,7 +345,7 @@ def test_client_auth_get_run(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, _, _, _, get_run = conn + _, _, _, _, get_run, _ = conn assert get_run is not None get_run(0) shared_secret = generate_shared_key( diff --git a/src/py/flwr/client/grpc_rere_client/connection.py b/src/py/flwr/client/grpc_rere_client/connection.py index 64543626e695..af74125140e9 100644 --- a/src/py/flwr/client/grpc_rere_client/connection.py +++ b/src/py/flwr/client/grpc_rere_client/connection.py @@ -45,7 +45,7 @@ message_to_taskres, user_config_from_proto, ) -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, DeleteNodeRequest, @@ -86,6 +86,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915 Optional[Callable[[], Optional[int]]], Optional[Callable[[], None]], Optional[Callable[[int], Run]], + Optional[Callable[[str], Fab]], ] ]: """Primitives for request/response-based interaction with a server. @@ -288,8 +289,12 @@ def get_run(run_id: int) -> Run: user_config_from_proto(get_run_response.run.override_config), ) + def get_fab(fab_hash: str) -> Fab: + # Call FleetAPI + raise NotImplementedError + try: # Yield methods - yield (receive, send, create_node, delete_node, get_run) + yield (receive, send, create_node, delete_node, get_run, get_fab) except Exception as exc: # pylint: disable=broad-except log(ERROR, exc) diff --git a/src/py/flwr/client/rest_client/connection.py b/src/py/flwr/client/rest_client/connection.py index e2bb1f62bc43..2da320622c17 100644 --- a/src/py/flwr/client/rest_client/connection.py +++ b/src/py/flwr/client/rest_client/connection.py @@ -45,7 +45,7 @@ message_to_taskres, user_config_from_proto, ) -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -97,6 +97,7 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915 Optional[Callable[[], Optional[int]]], Optional[Callable[[], None]], Optional[Callable[[int], Run]], + Optional[Callable[[str], Fab]], ] ]: """Primitives for request/response-based interaction with a server. @@ -366,8 +367,12 @@ def get_run(run_id: int) -> Run: user_config_from_proto(res.run.override_config), ) + def get_fab(fab_hash: str) -> Fab: + # Call FleetAPI + raise NotImplementedError + try: # Yield methods - yield (receive, send, create_node, delete_node, get_run) + yield (receive, send, create_node, delete_node, get_run, get_fab) except Exception as exc: # pylint: disable=broad-except log(ERROR, exc) From 2ed22bf026ddb360e743622d7581bdec6e839f85 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Tue, 13 Aug 2024 12:08:54 +0100 Subject: [PATCH 056/188] feat(framework) Add `ClientApp` IO serde (#3971) --- src/py/flwr/common/serde.py | 157 ++++++++++++++++++++++++++++++++++- src/py/flwr/common/typing.py | 16 ++++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/common/serde.py b/src/py/flwr/common/serde.py index 5e34b2b4b5f8..77bdd2c07d15 100644 --- a/src/py/flwr/common/serde.py +++ b/src/py/flwr/common/serde.py @@ -20,7 +20,11 @@ from google.protobuf.message import Message as GrpcMessage # pylint: disable=E0611 +from flwr.proto.clientappio_pb2 import ClientAppOutputCode, ClientAppOutputStatus from flwr.proto.error_pb2 import Error as ProtoError +from flwr.proto.message_pb2 import Context as ProtoContext +from flwr.proto.message_pb2 import Message as ProtoMessage +from flwr.proto.message_pb2 import Metadata as ProtoMetadata from flwr.proto.node_pb2 import Node from flwr.proto.recordset_pb2 import Array as ProtoArray from flwr.proto.recordset_pb2 import BoolList, BytesList @@ -32,6 +36,7 @@ from flwr.proto.recordset_pb2 import ParametersRecord as ProtoParametersRecord from flwr.proto.recordset_pb2 import RecordSet as ProtoRecordSet from flwr.proto.recordset_pb2 import Sint64List, StringList +from flwr.proto.run_pb2 import Run as ProtoRun from flwr.proto.task_pb2 import Task, TaskIns, TaskRes from flwr.proto.transport_pb2 import ( ClientMessage, @@ -44,7 +49,15 @@ ) # pylint: enable=E0611 -from . import Array, ConfigsRecord, MetricsRecord, ParametersRecord, RecordSet, typing +from . import ( + Array, + ConfigsRecord, + Context, + MetricsRecord, + ParametersRecord, + RecordSet, + typing, +) from .message import Error, Message, Metadata from .record.typeddict import TypedDict @@ -716,3 +729,145 @@ def user_config_value_from_proto(scalar_msg: Scalar) -> typing.UserConfigValue: scalar_field = scalar_msg.WhichOneof("scalar") scalar = getattr(scalar_msg, cast(str, scalar_field)) return cast(typing.UserConfigValue, scalar) + + +# === Metadata messages === + + +def metadata_to_proto(metadata: Metadata) -> ProtoMetadata: + """Serialize `Metadata` to ProtoBuf.""" + proto = ProtoMetadata( # pylint: disable=E1101 + run_id=metadata.run_id, + message_id=metadata.message_id, + src_node_id=metadata.src_node_id, + dst_node_id=metadata.dst_node_id, + reply_to_message=metadata.reply_to_message, + group_id=metadata.group_id, + ttl=metadata.ttl, + message_type=metadata.message_type, + ) + return proto + + +def metadata_from_proto(metadata_proto: ProtoMetadata) -> Metadata: + """Deserialize `Metadata` from ProtoBuf.""" + metadata = Metadata( + run_id=metadata_proto.run_id, + message_id=metadata_proto.message_id, + src_node_id=metadata_proto.src_node_id, + dst_node_id=metadata_proto.dst_node_id, + reply_to_message=metadata_proto.reply_to_message, + group_id=metadata_proto.group_id, + ttl=metadata_proto.ttl, + message_type=metadata_proto.message_type, + ) + return metadata + + +# === Message messages === + + +def message_to_proto(message: Message) -> ProtoMessage: + """Serialize `Message` to ProtoBuf.""" + proto = ProtoMessage( + metadata=metadata_to_proto(message.metadata), + content=recordset_to_proto(message.content), + error=error_to_proto(message.error) if message.has_error() else None, + ) + return proto + + +def message_from_proto(message_proto: ProtoMessage) -> Message: + """Deserialize `Message` from ProtoBuf.""" + message = Message( + metadata=metadata_from_proto(message_proto.metadata), + content=( + recordset_from_proto(message_proto.content) + if message_proto.HasField("content") + else None + ), + error=( + error_from_proto(message_proto.error) + if message_proto.HasField("error") + else None + ), + ) + return message + + +# === Context messages === + + +def context_to_proto(context: Context) -> ProtoContext: + """Serialize `Context` to ProtoBuf.""" + proto = ProtoContext( + node_id=context.node_id, + node_config=user_config_to_proto(context.node_config), + state=recordset_to_proto(context.state), + run_config=user_config_to_proto(context.run_config), + ) + return proto + + +def context_from_proto(context_proto: ProtoContext) -> Context: + """Deserialize `Context` from ProtoBuf.""" + context = Context( + node_id=context_proto.node_id, + node_config=user_config_from_proto(context_proto.node_config), + state=recordset_from_proto(context_proto.state), + run_config=user_config_from_proto(context_proto.run_config), + ) + return context + + +# === Run messages === + + +def run_to_proto(run: typing.Run) -> ProtoRun: + """Serialize `Run` to ProtoBuf.""" + proto = ProtoRun( + run_id=run.run_id, + fab_id=run.fab_id, + fab_version=run.fab_version, + override_config=user_config_to_proto(run.override_config), + fab_hash="", + ) + return proto + + +def run_from_proto(run_proto: ProtoRun) -> typing.Run: + """Deserialize `Run` from ProtoBuf.""" + run = typing.Run( + run_id=run_proto.run_id, + fab_id=run_proto.fab_id, + fab_version=run_proto.fab_version, + override_config=user_config_from_proto(run_proto.override_config), + ) + return run + + +# === ClientApp status messages === + + +def clientappstatus_to_proto( + status: typing.ClientAppOutputStatus, +) -> ClientAppOutputStatus: + """Serialize `ClientAppOutputStatus` to ProtoBuf.""" + code = ClientAppOutputCode.SUCCESS + if status.code == typing.ClientAppOutputCode.DEADLINE_EXCEEDED: + code = ClientAppOutputCode.DEADLINE_EXCEEDED + if status.code == typing.ClientAppOutputCode.UNKNOWN_ERROR: + code = ClientAppOutputCode.UNKNOWN_ERROR + return ClientAppOutputStatus(code=code, message=status.message) + + +def clientappstatus_from_proto( + msg: ClientAppOutputStatus, +) -> typing.ClientAppOutputStatus: + """Deserialize `ClientAppOutputStatus` from ProtoBuf.""" + code = typing.ClientAppOutputCode.SUCCESS + if msg.code == ClientAppOutputCode.DEADLINE_EXCEEDED: + code = typing.ClientAppOutputCode.DEADLINE_EXCEEDED + if msg.code == ClientAppOutputCode.UNKNOWN_ERROR: + code = typing.ClientAppOutputCode.UNKNOWN_ERROR + return typing.ClientAppOutputStatus(code=code, message=msg.message) diff --git a/src/py/flwr/common/typing.py b/src/py/flwr/common/typing.py index 0a48ab98059c..68a2b5825f06 100644 --- a/src/py/flwr/common/typing.py +++ b/src/py/flwr/common/typing.py @@ -83,6 +83,22 @@ class Status: message: str +class ClientAppOutputCode(Enum): + """ClientAppIO status codes.""" + + SUCCESS = 0 + DEADLINE_EXCEEDED = 1 + UNKNOWN_ERROR = 2 + + +@dataclass +class ClientAppOutputStatus: + """ClientAppIO status.""" + + code: ClientAppOutputCode + message: str + + @dataclass class Parameters: """Model parameters.""" From 354af13409ba98f37dcd7c4dad702c76ac3b6d37 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Tue, 13 Aug 2024 22:57:53 +0100 Subject: [PATCH 057/188] refactor(examples) Update Flower example for variational autoencoder with PyTorch (#3874) Co-authored-by: jafermarq --- .../.gitignore | 2 - .../README.md | 82 ++++----- .../client.py | 102 ----------- .../fedvaeexample/__init__.py | 1 + .../fedvaeexample/client_app.py | 53 ++++++ .../fedvaeexample/server_app.py | 28 +++ .../fedvaeexample/task.py | 159 ++++++++++++++++++ .../models.py | 69 -------- .../pyproject.toml | 49 ++++-- .../requirements.txt | 3 - .../server.py | 12 -- 11 files changed, 309 insertions(+), 251 deletions(-) delete mode 100644 examples/pytorch-federated-variational-autoencoder/.gitignore delete mode 100644 examples/pytorch-federated-variational-autoencoder/client.py create mode 100644 examples/pytorch-federated-variational-autoencoder/fedvaeexample/__init__.py create mode 100644 examples/pytorch-federated-variational-autoencoder/fedvaeexample/client_app.py create mode 100644 examples/pytorch-federated-variational-autoencoder/fedvaeexample/server_app.py create mode 100644 examples/pytorch-federated-variational-autoencoder/fedvaeexample/task.py delete mode 100644 examples/pytorch-federated-variational-autoencoder/models.py delete mode 100644 examples/pytorch-federated-variational-autoencoder/requirements.txt delete mode 100644 examples/pytorch-federated-variational-autoencoder/server.py diff --git a/examples/pytorch-federated-variational-autoencoder/.gitignore b/examples/pytorch-federated-variational-autoencoder/.gitignore deleted file mode 100644 index c3b84dc53dce..000000000000 --- a/examples/pytorch-federated-variational-autoencoder/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.gz -cifar-10-batches-py/ diff --git a/examples/pytorch-federated-variational-autoencoder/README.md b/examples/pytorch-federated-variational-autoencoder/README.md index 52f94a16307c..7b65406fe923 100644 --- a/examples/pytorch-federated-variational-autoencoder/README.md +++ b/examples/pytorch-federated-variational-autoencoder/README.md @@ -4,76 +4,60 @@ dataset: [CIFAR-10] framework: [torch, torchvision] --- -# Flower Example for Federated Variational Autoencoder using Pytorch +# Federated Variational Autoencoder with PyTorch and Flower -This example demonstrates how a variational autoencoder (VAE) can be trained in a federated way using the Flower framework. +This example demonstrates how a variational autoencoder (VAE) can be trained in a federated way using the Flower framework. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the CIFAR-10 dataset. -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/pytorch_federated_variational_autoencoder . && rm -rf flower && cd pytorch_federated_variational_autoencoder -``` - -This will create a new directory called `pytorch_federated_variational_autoencoder` containing the following files: - -```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- README.md --- models.py -``` - -### Installing Dependencies - -Project dependencies (such as `torch` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -#### Poetry +Start by cloning the example project: ```shell -poetry install -poetry shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/pytorch-federated-variational-autoencoder . \ + && rm -rf _tmp && cd pytorch-federated-variational-autoencoder ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +This will create a new directory called `pytorch-federated-variational-autoencoder` with the following structure: ```shell -poetry run python3 -c "import flwr" +pytorch-federated-variational-autoencoder +β”œβ”€β”€ README.md +β”œβ”€β”€ fedvaeexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +└── pyproject.toml # Project metadata like dependencies and configs ``` -If you don't see any errors you're good to go! +### Install dependencies and project -#### pip +Install the dependencies defined in `pyproject.toml` as well as the `fedvaeexample` package. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt +```bash +pip install -e . ``` -## Federating the Variational Autoencoder Model +## Run the Project -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: - -```shell -poetry run python3 server.py -``` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminals and run the following command in each: +### Run with the Simulation Engine -```shell -poetry run python3 client.py +```bash +flwr run . ``` -Alternatively you can run all of it in one shell as follows: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -poetry run python3 server.py & -poetry run python3 client.py & -poetry run python3 client.py +```bash +flwr run . --run-config num-server-rounds=5 ``` -You will see that the federated training of variational autoencoder has started. You can add `steps_per_epoch=3` to `model.fit()` if you just want to evaluate that everything works without having to wait for the client-side training to finish (this will save you a lot of time during development). +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/pytorch-federated-variational-autoencoder/client.py b/examples/pytorch-federated-variational-autoencoder/client.py deleted file mode 100644 index 9fa4707a7a45..000000000000 --- a/examples/pytorch-federated-variational-autoencoder/client.py +++ /dev/null @@ -1,102 +0,0 @@ -from collections import OrderedDict - -import flwr as fl -import torch -import torch.nn.functional as F -import torchvision.transforms as transforms -from torch.utils.data import DataLoader -from torchvision.datasets import CIFAR10 - -from models import Net - -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -def load_data(): - """Load CIFAR-10 (training and test set).""" - transform = transforms.Compose( - [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - trainset = CIFAR10(".", train=True, download=True, transform=transform) - testset = CIFAR10(".", train=False, download=True, transform=transform) - trainloader = DataLoader(trainset, batch_size=32, shuffle=True) - testloader = DataLoader(testset, batch_size=32) - return trainloader, testloader - - -def train(net, trainloader, epochs): - """Train the network on the training set.""" - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) - for _ in range(epochs): - for images, _ in trainloader: - images = images.to(DEVICE) - optimizer.zero_grad() - recon_images, mu, logvar = net(images) - recon_loss = F.mse_loss(recon_images, images) - kld_loss = -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp()) - loss = recon_loss + 0.05 * kld_loss - loss.backward() - optimizer.step() - - -def test(net, testloader): - """Validate the network on the entire test set.""" - total, loss = 0, 0.0 - with torch.no_grad(): - for data in testloader: - images = data[0].to(DEVICE) - recon_images, mu, logvar = net(images) - recon_loss = F.mse_loss(recon_images, images) - kld_loss = -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp()) - loss += recon_loss + kld_loss - total += len(images) - return loss / total - - -def sample(net): - """Generates samples using the decoder of the trained VAE.""" - with torch.no_grad(): - z = torch.randn(10) - z = z.to(DEVICE) - gen_image = net.decode(z) - return gen_image - - -def generate(net, image): - """Reproduce the input with trained VAE.""" - with torch.no_grad(): - return net.forward(image) - - -def main(): - # Load model and data - net = Net() - net = net.to(DEVICE) - trainloader, testloader = load_data() - - class CifarClient(fl.client.NumPyClient): - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - train(net, trainloader, epochs=1) - return self.get_parameters(config={}), len(trainloader), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss = test(net, testloader) - return float(loss), len(testloader), {} - - fl.client.start_client( - server_address="127.0.0.1:8080", client=CifarClient().to_client() - ) - - -if __name__ == "__main__": - main() diff --git a/examples/pytorch-federated-variational-autoencoder/fedvaeexample/__init__.py b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/__init__.py new file mode 100644 index 000000000000..08622fc6f28f --- /dev/null +++ b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/__init__.py @@ -0,0 +1 @@ +"""fedvaeexample: A Flower / PyTorch app for Federated Variational Autoencoder.""" diff --git a/examples/pytorch-federated-variational-autoencoder/fedvaeexample/client_app.py b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/client_app.py new file mode 100644 index 000000000000..6a0508e18d4c --- /dev/null +++ b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/client_app.py @@ -0,0 +1,53 @@ +"""fedvaeexample: A Flower / PyTorch app for Federated Variational Autoencoder.""" + +import torch +from fedvaeexample.task import Net, get_weights, load_data, set_weights, test, train + +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context + + +class CifarClient(NumPyClient): + def __init__(self, trainloader, testloader, local_epochs, learning_rate): + self.net = Net() + self.trainloader = trainloader + self.testloader = testloader + self.local_epochs = local_epochs + self.lr = learning_rate + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + def fit(self, parameters, config): + """Train the model with data of this client.""" + set_weights(self.net, parameters) + train( + self.net, + self.trainloader, + epochs=self.local_epochs, + learning_rate=self.lr, + device=self.device, + ) + return get_weights(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_weights(self.net, parameters) + loss = test(self.net, self.testloader, self.device) + return float(loss), len(self.testloader), {} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + # Read the run_config to fetch hyperparameters relevant to this run + trainloader, testloader = load_data(partition_id, num_partitions) + local_epochs = context.run_config["local-epochs"] + learning_rate = context.run_config["learning-rate"] + + return CifarClient(trainloader, testloader, local_epochs, learning_rate).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/pytorch-federated-variational-autoencoder/fedvaeexample/server_app.py b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/server_app.py new file mode 100644 index 000000000000..0f7a6520de59 --- /dev/null +++ b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/server_app.py @@ -0,0 +1,28 @@ +"""fedvaeexample: A Flower / PyTorch app for Federated Variational Autoencoder.""" + +from fedvaeexample.task import Net, get_weights + +from flwr.common import Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components for ServerApp.""" + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define the strategy + strategy = FedAvg(initial_parameters=parameters) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/pytorch-federated-variational-autoencoder/fedvaeexample/task.py b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/task.py new file mode 100644 index 000000000000..112bd3358fb0 --- /dev/null +++ b/examples/pytorch-federated-variational-autoencoder/fedvaeexample/task.py @@ -0,0 +1,159 @@ +"""fedvae: A Flower app for Federated Variational Autoencoder.""" + +from collections import OrderedDict + +import torch +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from torch import nn +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor + + +class Flatten(nn.Module): + """Flattens input by reshaping it into a one-dimensional tensor.""" + + def forward(self, input): + return input.view(input.size(0), -1) + + +class UnFlatten(nn.Module): + """Unflattens a tensor converting it to a desired shape.""" + + def forward(self, input): + return input.view(-1, 16, 6, 6) + + +class Net(nn.Module): + def __init__(self, h_dim=576, z_dim=10) -> None: + super().__init__() + self.encoder = nn.Sequential( + nn.Conv2d( + in_channels=3, out_channels=6, kernel_size=4, stride=2 + ), # [batch, 6, 15, 15] + nn.ReLU(), + nn.Conv2d( + in_channels=6, out_channels=16, kernel_size=5, stride=2 + ), # [batch, 16, 6, 6] + nn.ReLU(), + Flatten(), + ) + + self.fc1 = nn.Linear(h_dim, z_dim) + self.fc2 = nn.Linear(h_dim, z_dim) + self.fc3 = nn.Linear(z_dim, h_dim) + + self.decoder = nn.Sequential( + UnFlatten(), + nn.ConvTranspose2d(in_channels=16, out_channels=6, kernel_size=5, stride=2), + nn.ReLU(), + nn.ConvTranspose2d(in_channels=6, out_channels=3, kernel_size=4, stride=2), + nn.Tanh(), + ) + + def reparametrize(self, h): + """Reparametrization layer of VAE.""" + mu, logvar = self.fc1(h), self.fc2(h) + std = torch.exp(logvar / 2) + eps = torch.randn_like(std) + z = mu + std * eps + return z, mu, logvar + + def encode(self, x): + """Encoder of the VAE.""" + h = self.encoder(x) + z, mu, logvar = self.reparametrize(h) + return z, mu, logvar + + def decode(self, z): + """Decoder of the VAE.""" + z = self.fc3(z) + z = self.decoder(z) + return z + + def forward(self, x): + z, mu, logvar = self.encode(x) + z_decode = self.decode(z) + return z_decode, mu, logvar + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id, num_partitions): + """Load partition CIFAR10 data.""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + return trainloader, testloader + + +def train(net, trainloader, epochs, learning_rate, device): + """Train the network on the training set.""" + net.to(device) # move model to GPU if available + optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) + for _ in range(epochs): + # for images, _ in trainloader: + for batch in trainloader: + images = batch["img"] + images = images.to(device) + optimizer.zero_grad() + recon_images, mu, logvar = net(images) + recon_loss = F.mse_loss(recon_images, images) + kld_loss = -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp()) + loss = recon_loss + 0.05 * kld_loss + loss.backward() + optimizer.step() + + +def test(net, testloader, device): + """Validate the network on the entire test set.""" + total, loss = 0, 0.0 + with torch.no_grad(): + # for data in testloader: + for batch in testloader: + images = batch["img"].to(device) + # images = data[0].to(DEVICE) + recon_images, mu, logvar = net(images) + recon_loss = F.mse_loss(recon_images, images) + kld_loss = -0.5 * torch.mean(1 + logvar - mu.pow(2) - logvar.exp()) + loss += recon_loss + kld_loss + total += len(images) + return loss / total + + +def generate(net, image): + """Reproduce the input with trained VAE.""" + with torch.no_grad(): + return net.forward(image) + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) diff --git a/examples/pytorch-federated-variational-autoencoder/models.py b/examples/pytorch-federated-variational-autoencoder/models.py deleted file mode 100644 index 4999631051b2..000000000000 --- a/examples/pytorch-federated-variational-autoencoder/models.py +++ /dev/null @@ -1,69 +0,0 @@ -import torch -import torch.nn as nn - - -class Flatten(nn.Module): - """Flattens input by reshaping it into a one-dimensional tensor.""" - - def forward(self, input): - return input.view(input.size(0), -1) - - -class UnFlatten(nn.Module): - """Unflattens a tensor converting it to a desired shape.""" - - def forward(self, input): - return input.view(-1, 16, 6, 6) - - -class Net(nn.Module): - def __init__(self, h_dim=576, z_dim=10) -> None: - super(Net, self).__init__() - self.encoder = nn.Sequential( - nn.Conv2d( - in_channels=3, out_channels=6, kernel_size=4, stride=2 - ), # [batch, 6, 15, 15] - nn.ReLU(), - nn.Conv2d( - in_channels=6, out_channels=16, kernel_size=5, stride=2 - ), # [batch, 16, 6, 6] - nn.ReLU(), - Flatten(), - ) - - self.fc1 = nn.Linear(h_dim, z_dim) - self.fc2 = nn.Linear(h_dim, z_dim) - self.fc3 = nn.Linear(z_dim, h_dim) - - self.decoder = nn.Sequential( - UnFlatten(), - nn.ConvTranspose2d(in_channels=16, out_channels=6, kernel_size=5, stride=2), - nn.ReLU(), - nn.ConvTranspose2d(in_channels=6, out_channels=3, kernel_size=4, stride=2), - nn.Tanh(), - ) - - def reparametrize(self, h): - """Reparametrization layer of VAE.""" - mu, logvar = self.fc1(h), self.fc2(h) - std = torch.exp(logvar / 2) - eps = torch.randn_like(std) - z = mu + std * eps - return z, mu, logvar - - def encode(self, x): - """Encoder of the VAE.""" - h = self.encoder(x) - z, mu, logvar = self.reparametrize(h) - return z, mu, logvar - - def decode(self, z): - """Decoder of the VAE.""" - z = self.fc3(z) - z = self.decoder(z) - return z - - def forward(self, x): - z, mu, logvar = self.encode(x) - z_decode = self.decode(z) - return z_decode, mu, logvar diff --git a/examples/pytorch-federated-variational-autoencoder/pyproject.toml b/examples/pytorch-federated-variational-autoencoder/pyproject.toml index bc1f85803682..5109eaf4d2e2 100644 --- a/examples/pytorch-federated-variational-autoencoder/pyproject.toml +++ b/examples/pytorch-federated-variational-autoencoder/pyproject.toml @@ -1,15 +1,36 @@ -[tool.poetry] -name = "pytorch_federated_variational_autoencoder" -version = "0.1.0" -description = "Federated Variational Autoencoder Example" -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -torch = "1.13.1" -torchvision = "0.14.1" - [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "fedvaeexample" +version = "1.0.0" +description = "Federated Variational Autoencoder Example with PyTorch and Flower" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "fedvaeexample.server_app:app" +clientapp = "fedvaeexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +local-epochs = 1 +learning-rate = 0.001 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 2 diff --git a/examples/pytorch-federated-variational-autoencoder/requirements.txt b/examples/pytorch-federated-variational-autoencoder/requirements.txt deleted file mode 100644 index f3caddbc875e..000000000000 --- a/examples/pytorch-federated-variational-autoencoder/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flwr>=1.0, <2.0 -torch==1.13.1 -torchvision==0.14.1 diff --git a/examples/pytorch-federated-variational-autoencoder/server.py b/examples/pytorch-federated-variational-autoencoder/server.py deleted file mode 100644 index 575ebb2235e2..000000000000 --- a/examples/pytorch-federated-variational-autoencoder/server.py +++ /dev/null @@ -1,12 +0,0 @@ -import flwr as fl - - -def main(): - fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - ) - - -if __name__ == "__main__": - main() From 5d7f1f20837c44e9735bdd32f54b659945e1ef3d Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Thu, 15 Aug 2024 10:35:55 +0100 Subject: [PATCH 058/188] fix(framework) Add `created_at` to `Metadata` proto (#4016) --- src/proto/flwr/proto/message.proto | 1 + src/py/flwr/proto/message_pb2.py | 4 ++-- src/py/flwr/proto/message_pb2.pyi | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/proto/flwr/proto/message.proto b/src/proto/flwr/proto/message.proto index d568522e761e..3230ab0609a9 100644 --- a/src/proto/flwr/proto/message.proto +++ b/src/proto/flwr/proto/message.proto @@ -43,4 +43,5 @@ message Metadata { string group_id = 6; double ttl = 7; string message_type = 8; + double created_at = 9; } diff --git a/src/py/flwr/proto/message_pb2.py b/src/py/flwr/proto/message_pb2.py index 1dfa5656ea79..7e2555972a8a 100644 --- a/src/py/flwr/proto/message_pb2.py +++ b/src/py/flwr/proto/message_pb2.py @@ -17,7 +17,7 @@ from flwr.proto import transport_pb2 as flwr_dot_proto_dot_transport__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/message.proto\x12\nflwr.proto\x1a\x16\x66lwr/proto/error.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x1a\x66lwr/proto/transport.proto\"{\n\x07Message\x12&\n\x08metadata\x18\x01 \x01(\x0b\x32\x14.flwr.proto.Metadata\x12&\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12 \n\x05\x65rror\x18\x03 \x01(\x0b\x32\x11.flwr.proto.Error\"\xbf\x02\n\x07\x43ontext\x12\x0f\n\x07node_id\x18\x01 \x01(\x12\x12\x38\n\x0bnode_config\x18\x02 \x03(\x0b\x32#.flwr.proto.Context.NodeConfigEntry\x12$\n\x05state\x18\x03 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12\x36\n\nrun_config\x18\x04 \x03(\x0b\x32\".flwr.proto.Context.RunConfigEntry\x1a\x45\n\x0fNodeConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\x1a\x44\n\x0eRunConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\xa7\x01\n\x08Metadata\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x12\x12\n\nmessage_id\x18\x02 \x01(\t\x12\x13\n\x0bsrc_node_id\x18\x03 \x01(\x12\x12\x13\n\x0b\x64st_node_id\x18\x04 \x01(\x12\x12\x18\n\x10reply_to_message\x18\x05 \x01(\t\x12\x10\n\x08group_id\x18\x06 \x01(\t\x12\x0b\n\x03ttl\x18\x07 \x01(\x01\x12\x14\n\x0cmessage_type\x18\x08 \x01(\tb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/message.proto\x12\nflwr.proto\x1a\x16\x66lwr/proto/error.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x1a\x66lwr/proto/transport.proto\"{\n\x07Message\x12&\n\x08metadata\x18\x01 \x01(\x0b\x32\x14.flwr.proto.Metadata\x12&\n\x07\x63ontent\x18\x02 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12 \n\x05\x65rror\x18\x03 \x01(\x0b\x32\x11.flwr.proto.Error\"\xbf\x02\n\x07\x43ontext\x12\x0f\n\x07node_id\x18\x01 \x01(\x12\x12\x38\n\x0bnode_config\x18\x02 \x03(\x0b\x32#.flwr.proto.Context.NodeConfigEntry\x12$\n\x05state\x18\x03 \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12\x36\n\nrun_config\x18\x04 \x03(\x0b\x32\".flwr.proto.Context.RunConfigEntry\x1a\x45\n\x0fNodeConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\x1a\x44\n\x0eRunConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\xbb\x01\n\x08Metadata\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\x12\x12\n\nmessage_id\x18\x02 \x01(\t\x12\x13\n\x0bsrc_node_id\x18\x03 \x01(\x12\x12\x13\n\x0b\x64st_node_id\x18\x04 \x01(\x12\x12\x18\n\x10reply_to_message\x18\x05 \x01(\t\x12\x10\n\x08group_id\x18\x06 \x01(\t\x12\x0b\n\x03ttl\x18\x07 \x01(\x01\x12\x14\n\x0cmessage_type\x18\x08 \x01(\t\x12\x12\n\ncreated_at\x18\t \x01(\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,5 +37,5 @@ _globals['_CONTEXT_RUNCONFIGENTRY']._serialized_start=497 _globals['_CONTEXT_RUNCONFIGENTRY']._serialized_end=565 _globals['_METADATA']._serialized_start=568 - _globals['_METADATA']._serialized_end=735 + _globals['_METADATA']._serialized_end=755 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/message_pb2.pyi b/src/py/flwr/proto/message_pb2.pyi index 68b98430a59a..b352917f217e 100644 --- a/src/py/flwr/proto/message_pb2.pyi +++ b/src/py/flwr/proto/message_pb2.pyi @@ -99,6 +99,7 @@ class Metadata(google.protobuf.message.Message): GROUP_ID_FIELD_NUMBER: builtins.int TTL_FIELD_NUMBER: builtins.int MESSAGE_TYPE_FIELD_NUMBER: builtins.int + CREATED_AT_FIELD_NUMBER: builtins.int run_id: builtins.int message_id: typing.Text src_node_id: builtins.int @@ -107,6 +108,7 @@ class Metadata(google.protobuf.message.Message): group_id: typing.Text ttl: builtins.float message_type: typing.Text + created_at: builtins.float def __init__(self, *, run_id: builtins.int = ..., @@ -117,6 +119,7 @@ class Metadata(google.protobuf.message.Message): group_id: typing.Text = ..., ttl: builtins.float = ..., message_type: typing.Text = ..., + created_at: builtins.float = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["dst_node_id",b"dst_node_id","group_id",b"group_id","message_id",b"message_id","message_type",b"message_type","reply_to_message",b"reply_to_message","run_id",b"run_id","src_node_id",b"src_node_id","ttl",b"ttl"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["created_at",b"created_at","dst_node_id",b"dst_node_id","group_id",b"group_id","message_id",b"message_id","message_type",b"message_type","reply_to_message",b"reply_to_message","run_id",b"run_id","src_node_id",b"src_node_id","ttl",b"ttl"]) -> None: ... global___Metadata = Metadata From e5aba922c0114bfd4c900d46e15b46baf8236b4c Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 11:45:46 +0200 Subject: [PATCH 059/188] feat(framework) Add FAB serde functions (#3945) --- src/py/flwr/common/serde.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/py/flwr/common/serde.py b/src/py/flwr/common/serde.py index 77bdd2c07d15..6c22670f880e 100644 --- a/src/py/flwr/common/serde.py +++ b/src/py/flwr/common/serde.py @@ -22,6 +22,7 @@ # pylint: disable=E0611 from flwr.proto.clientappio_pb2 import ClientAppOutputCode, ClientAppOutputStatus from flwr.proto.error_pb2 import Error as ProtoError +from flwr.proto.fab_pb2 import Fab as ProtoFab from flwr.proto.message_pb2 import Context as ProtoContext from flwr.proto.message_pb2 import Message as ProtoMessage from flwr.proto.message_pb2 import Metadata as ProtoMetadata @@ -686,6 +687,19 @@ def message_from_taskres(taskres: TaskRes) -> Message: return message +# === FAB === + + +def fab_to_proto(fab: typing.Fab) -> ProtoFab: + """Create a proto Fab object from a Python Fab.""" + return ProtoFab(hash_str=fab.hash_str, content=fab.content) + + +def fab_from_proto(fab: ProtoFab) -> typing.Fab: + """Create a Python Fab object from a proto Fab.""" + return typing.Fab(fab.hash_str, fab.content) + + # === User configs === From 747959a564f8f35ee31bd0a09e2c0f756c316b14 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Thu, 15 Aug 2024 11:14:12 +0100 Subject: [PATCH 060/188] feat(framework) Add `ClientAppIo` serde tests (#4002) Co-authored-by: Javier --- src/py/flwr/common/serde.py | 9 +- src/py/flwr/common/serde_test.py | 151 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/common/serde.py b/src/py/flwr/common/serde.py index 6c22670f880e..819b113d041b 100644 --- a/src/py/flwr/common/serde.py +++ b/src/py/flwr/common/serde.py @@ -759,6 +759,7 @@ def metadata_to_proto(metadata: Metadata) -> ProtoMetadata: group_id=metadata.group_id, ttl=metadata.ttl, message_type=metadata.message_type, + created_at=metadata.created_at, ) return proto @@ -785,7 +786,9 @@ def message_to_proto(message: Message) -> ProtoMessage: """Serialize `Message` to ProtoBuf.""" proto = ProtoMessage( metadata=metadata_to_proto(message.metadata), - content=recordset_to_proto(message.content), + content=( + recordset_to_proto(message.content) if message.has_content() else None + ), error=error_to_proto(message.error) if message.has_error() else None, ) return proto @@ -793,6 +796,7 @@ def message_to_proto(message: Message) -> ProtoMessage: def message_from_proto(message_proto: ProtoMessage) -> Message: """Deserialize `Message` from ProtoBuf.""" + created_at = message_proto.metadata.created_at message = Message( metadata=metadata_from_proto(message_proto.metadata), content=( @@ -806,6 +810,9 @@ def message_from_proto(message_proto: ProtoMessage) -> Message: else None ), ) + # `.created_at` is set upon Message object construction + # we need to manually set it to the original value + message.metadata.created_at = created_at return message diff --git a/src/py/flwr/common/serde_test.py b/src/py/flwr/common/serde_test.py index afb11b6956f2..8f6de3152ebf 100644 --- a/src/py/flwr/common/serde_test.py +++ b/src/py/flwr/common/serde_test.py @@ -21,23 +21,41 @@ import pytest # pylint: disable=E0611 +from flwr.proto import clientappio_pb2 from flwr.proto import transport_pb2 as pb2 +from flwr.proto.message_pb2 import Context as ProtoContext +from flwr.proto.message_pb2 import Message as ProtoMessage from flwr.proto.recordset_pb2 import Array as ProtoArray from flwr.proto.recordset_pb2 import ConfigsRecord as ProtoConfigsRecord from flwr.proto.recordset_pb2 import MetricsRecord as ProtoMetricsRecord from flwr.proto.recordset_pb2 import ParametersRecord as ProtoParametersRecord from flwr.proto.recordset_pb2 import RecordSet as ProtoRecordSet +from flwr.proto.run_pb2 import Run as ProtoRun # pylint: enable=E0611 -from . import Array, ConfigsRecord, MetricsRecord, ParametersRecord, RecordSet, typing +from . import ( + Array, + ConfigsRecord, + Context, + MetricsRecord, + ParametersRecord, + RecordSet, + typing, +) from .message import Error, Message, Metadata from .serde import ( array_from_proto, array_to_proto, + clientappstatus_from_proto, + clientappstatus_to_proto, configs_record_from_proto, configs_record_to_proto, + context_from_proto, + context_to_proto, + message_from_proto, message_from_taskins, message_from_taskres, + message_to_proto, message_to_taskins, message_to_taskres, metrics_record_from_proto, @@ -46,6 +64,8 @@ parameters_record_to_proto, recordset_from_proto, recordset_to_proto, + run_from_proto, + run_to_proto, scalar_from_proto, scalar_to_proto, status_from_proto, @@ -223,6 +243,15 @@ def metadata(self) -> Metadata: message_type=self.get_str(10), ) + def user_config(self) -> typing.UserConfig: + """Create a UserConfig.""" + return { + "key1": self.rng.randint(0, 1 << 30), + "key2": self.get_str(10), + "key3": self.rng.random(), + "key4": bool(self.rng.getrandbits(1)), + } + def test_array_serialization_deserialization() -> None: """Test serialization and deserialization of Array.""" @@ -387,3 +416,123 @@ def test_message_to_and_from_taskres( if original.has_error(): assert original.error == deserialized.error assert metadata == deserialized.metadata + + +@pytest.mark.parametrize( + "content_fn, error_fn", + [ + ( + lambda maker: maker.recordset(1, 1, 1), + None, + ), # check when only content is set + (None, lambda code: Error(code=code)), # check when only error is set + ], +) +def test_message_serialization_deserialization( + content_fn: Callable[ + [ + RecordMaker, + ], + RecordSet, + ], + error_fn: Callable[[int], Error], +) -> None: + """Test serialization and deserialization of Message.""" + # Prepare + maker = RecordMaker(state=2) + metadata = maker.metadata() + metadata.dst_node_id = 0 # Assume driver node + + original = Message( + metadata=metadata, + content=None if content_fn is None else content_fn(maker), + error=None if error_fn is None else error_fn(0), + ) + + # Execute + proto = message_to_proto(original) + deserialized = message_from_proto(proto) + + # Assert + assert isinstance(proto, ProtoMessage) + + if original.has_content(): + assert original.content == deserialized.content + if original.has_error(): + assert original.error == deserialized.error + + assert original.metadata == deserialized.metadata + + +def test_context_serialization_deserialization() -> None: + """Test serialization and deserialization of Context.""" + # Prepare + maker = RecordMaker() + original = Context( + node_id=1, + node_config=maker.user_config(), + state=maker.recordset(1, 1, 1), + run_config=maker.user_config(), + ) + + # Execute + proto = context_to_proto(original) + deserialized = context_from_proto(proto) + + # Assert + assert isinstance(proto, ProtoContext) + assert original == deserialized + + +def test_run_serialization_deserialization() -> None: + """Test serialization and deserialization of Run.""" + # Prepare + maker = RecordMaker() + original = typing.Run( + run_id=1, + fab_id="lorem", + fab_version="ipsum", + override_config=maker.user_config(), + ) + + # Execute + proto = run_to_proto(original) + deserialized = run_from_proto(proto) + + # Assert + assert isinstance(proto, ProtoRun) + assert original == deserialized + + +def test_clientappstatus_to_proto() -> None: + """Test ClientApp status message (de-)serialization.""" + # Prepare + # pylint: disable=E1101 + code_msg = clientappio_pb2.ClientAppOutputCode.SUCCESS + status_msg = clientappio_pb2.ClientAppOutputStatus(code=code_msg, message="Success") + + code = typing.ClientAppOutputCode.SUCCESS + status = typing.ClientAppOutputStatus(code=code, message="Success") + + # Execute + actual_status_msg = clientappstatus_to_proto(status=status) + + # Assert + assert actual_status_msg == status_msg + + +def test_clientappstatus_from_proto() -> None: + """Test ClientApp status message (de-)serialization.""" + # Prepare + # pylint: disable=E1101 + code_msg = clientappio_pb2.ClientAppOutputCode.SUCCESS + status_msg = clientappio_pb2.ClientAppOutputStatus(code=code_msg, message="Success") + + code = typing.ClientAppOutputCode.SUCCESS + status = typing.ClientAppOutputStatus(code=code, message="Success") + + # Execute + actual_status = clientappstatus_from_proto(msg=status_msg) + + # Assert + assert actual_status == status From 4ae5e317d11c43e726e024d1ebeb5d43b6390f11 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 12:27:20 +0200 Subject: [PATCH 061/188] ci(framework) Add tests for FAB serde functions (#4017) --- src/py/flwr/common/serde_test.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/py/flwr/common/serde_test.py b/src/py/flwr/common/serde_test.py index 8f6de3152ebf..f3279c10e347 100644 --- a/src/py/flwr/common/serde_test.py +++ b/src/py/flwr/common/serde_test.py @@ -23,6 +23,7 @@ # pylint: disable=E0611 from flwr.proto import clientappio_pb2 from flwr.proto import transport_pb2 as pb2 +from flwr.proto.fab_pb2 import Fab as ProtoFab from flwr.proto.message_pb2 import Context as ProtoContext from flwr.proto.message_pb2 import Message as ProtoMessage from flwr.proto.recordset_pb2 import Array as ProtoArray @@ -52,6 +53,8 @@ configs_record_to_proto, context_from_proto, context_to_proto, + fab_from_proto, + fab_to_proto, message_from_proto, message_from_taskins, message_from_taskres, @@ -120,6 +123,30 @@ def test_status_from_proto() -> None: assert actual_status == status +def test_fab_to_proto() -> None: + """Test Fab serialization.""" + proto_fab = ProtoFab(hash_str="fab_test_hash", content=b"fab_test_content") + + py_fab = typing.Fab(hash_str="fab_test_hash", content=b"fab_test_content") + + converted_fab = fab_to_proto(py_fab) + + # Assert + assert converted_fab == proto_fab + + +def test_fab_from_proto() -> None: + """Test Fab deserialization.""" + proto_fab = ProtoFab(hash_str="fab_test_hash", content=b"fab_test_content") + + py_fab = typing.Fab(hash_str="fab_test_hash", content=b"fab_test_content") + + converted_fab = fab_from_proto(proto_fab) + + # Assert + assert converted_fab == py_fab + + T = TypeVar("T") From a0029979cf3a58b359edff09980b1f4ee74f5f7b Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 13:32:09 +0200 Subject: [PATCH 062/188] feat(framework) Add Ffs factory (#3999) Co-authored-by: Daniel J. Beutel --- .../flwr/server/superlink/ffs/ffs_factory.py | 47 +++++++++++++++++++ .../server/superlink/ffs/ffs_factory_test.py | 43 +++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/py/flwr/server/superlink/ffs/ffs_factory.py create mode 100644 src/py/flwr/server/superlink/ffs/ffs_factory_test.py diff --git a/src/py/flwr/server/superlink/ffs/ffs_factory.py b/src/py/flwr/server/superlink/ffs/ffs_factory.py new file mode 100644 index 000000000000..63ee5dc77c0a --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_factory.py @@ -0,0 +1,47 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Factory class that creates Ffs instances.""" + + +from logging import DEBUG +from typing import Optional + +from flwr.common.logger import log + +from .disk_ffs import DiskFfs +from .ffs import Ffs + + +class FfsFactory: + """Factory class that creates Ffs instances. + + Parameters + ---------- + base_dir : str + The base directory used by DiskFfs to store objects. + """ + + def __init__(self, base_dir: str) -> None: + self.base_dir = base_dir + self.ffs_instance: Optional[Ffs] = None + + def ffs(self) -> Ffs: + """Return a Ffs instance and create it, if necessary.""" + if not self.ffs_instance: + log(DEBUG, "Initializing DiskFfs") + self.ffs_instance = DiskFfs(self.base_dir) + + log(DEBUG, "Using DiskFfs") + return self.ffs_instance diff --git a/src/py/flwr/server/superlink/ffs/ffs_factory_test.py b/src/py/flwr/server/superlink/ffs/ffs_factory_test.py new file mode 100644 index 000000000000..81fcb6454147 --- /dev/null +++ b/src/py/flwr/server/superlink/ffs/ffs_factory_test.py @@ -0,0 +1,43 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Test Ffs factory.""" + +from .disk_ffs import DiskFfs +from .ffs_factory import FfsFactory + + +def test_disk_ffs_factory() -> None: + """Test DiskFfs instantiation with FfsFactory.""" + # Prepare + ffs_factory = FfsFactory("test") + + # Execute + ffs = ffs_factory.ffs() + + # Assert + assert isinstance(ffs, DiskFfs) + + +def test_cache_ffs_factory() -> None: + """Test cache with FfsFactory.""" + # Prepare + ffs_factory = FfsFactory("other_test") + ffs = ffs_factory.ffs() + + # Execute + other_ffs = ffs_factory.ffs() + + # Assert + assert id(ffs) == id(other_ffs) From 19f88bd6e5577de4be8f2bc7ee5a418304ab88ca Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 13:38:08 +0200 Subject: [PATCH 063/188] refactor(framework) Make fuse_dicts public for FAB delivery (#4011) --- src/py/flwr/common/config.py | 9 +++++++-- src/py/flwr/common/config_test.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index 5c647b572db5..a319b3cdc704 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -74,10 +74,15 @@ def get_project_config(project_dir: Union[str, Path]) -> Dict[str, Any]: return config -def _fuse_dicts( +def fuse_dicts( main_dict: UserConfig, override_dict: UserConfig, ) -> UserConfig: + """Merge a config with the overrides. + + Remove the nesting by adding the nested keys as prefixes separated by dots, and fuse + it with the override dict. + """ fused_dict = main_dict.copy() for key, value in override_dict.items(): @@ -96,7 +101,7 @@ def get_fused_config_from_dir( ) flat_default_config = flatten_dict(default_config) - return _fuse_dicts(flat_default_config, override_config) + return fuse_dicts(flat_default_config, override_config) def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig: diff --git a/src/py/flwr/common/config_test.py b/src/py/flwr/common/config_test.py index 0e6a5bb8cb9a..071263ed8531 100644 --- a/src/py/flwr/common/config_test.py +++ b/src/py/flwr/common/config_test.py @@ -24,8 +24,8 @@ from flwr.common.typing import UserConfig from .config import ( - _fuse_dicts, flatten_dict, + fuse_dicts, get_flwr_dir, get_project_config, get_project_dir, @@ -140,7 +140,7 @@ def test_get_fused_config_valid(tmp_path: Path) -> None: "config", {} ) - config = _fuse_dicts(flatten_dict(default_config), overrides) + config = fuse_dicts(flatten_dict(default_config), overrides) # Assert assert config == expected_config From 0b3b6123a23dd6da4a1d98bcd2d2a165b81bf051 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 13:52:32 +0200 Subject: [PATCH 064/188] feat(framework:skip) Replace bytes with FAB in exec proto (#4003) --- src/proto/flwr/proto/exec.proto | 3 +- src/py/flwr/cli/run/run.py | 8 ++++-- src/py/flwr/proto/exec_pb2.py | 31 +++++++++++---------- src/py/flwr/proto/exec_pb2.pyi | 11 +++++--- src/py/flwr/superexec/exec_servicer.py | 2 +- src/py/flwr/superexec/exec_servicer_test.py | 2 +- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/proto/flwr/proto/exec.proto b/src/proto/flwr/proto/exec.proto index 047b0d0910ff..65faf4386ea0 100644 --- a/src/proto/flwr/proto/exec.proto +++ b/src/proto/flwr/proto/exec.proto @@ -17,6 +17,7 @@ syntax = "proto3"; package flwr.proto; +import "flwr/proto/fab.proto"; import "flwr/proto/transport.proto"; service Exec { @@ -28,7 +29,7 @@ service Exec { } message StartRunRequest { - bytes fab_file = 1; + Fab fab = 1; map override_config = 2; map federation_config = 3; } diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index f0a98af3a724..fe7b2f32a104 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -14,6 +14,7 @@ # ============================================================================== """Flower command line interface `run` command.""" +import hashlib import subprocess import sys from logging import DEBUG @@ -28,7 +29,8 @@ from flwr.common.config import flatten_dict, parse_config_args from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel from flwr.common.logger import log -from flwr.common.serde import user_config_to_proto +from flwr.common.serde import fab_to_proto, user_config_to_proto +from flwr.common.typing import Fab from flwr.proto.exec_pb2 import StartRunRequest # pylint: disable=E0611 from flwr.proto.exec_pb2_grpc import ExecStub @@ -163,9 +165,11 @@ def on_channel_state_change(channel_connectivity: str) -> None: stub = ExecStub(channel) fab_path = Path(build(app)) + content = fab_path.read_bytes() + fab = Fab(hashlib.sha256(content).hexdigest(), content) req = StartRunRequest( - fab_file=fab_path.read_bytes(), + fab=fab_to_proto(fab), override_config=user_config_to_proto( parse_config_args(config_overrides, separator=",") ), diff --git a/src/py/flwr/proto/exec_pb2.py b/src/py/flwr/proto/exec_pb2.py index 6dfb061aff90..3fe109067296 100644 --- a/src/py/flwr/proto/exec_pb2.py +++ b/src/py/flwr/proto/exec_pb2.py @@ -12,10 +12,11 @@ _sym_db = _symbol_database.Default() +from flwr.proto import fab_pb2 as flwr_dot_proto_dot_fab__pb2 from flwr.proto import transport_pb2 as flwr_dot_proto_dot_transport__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xd3\x02\n\x0fStartRunRequest\x12\x10\n\x08\x66\x61\x62_file\x18\x01 \x01(\x0c\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12L\n\x11\x66\x65\x64\x65ration_config\x18\x03 \x03(\x0b\x32\x31.flwr.proto.StartRunRequest.FederationConfigEntry\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\x1aK\n\x15\x46\x65\x64\x65rationConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\"\n\x10StartRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"#\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"(\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t2\xa0\x01\n\x04\x45xec\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/exec.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\"\xdf\x02\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12L\n\x11\x66\x65\x64\x65ration_config\x18\x03 \x03(\x0b\x32\x31.flwr.proto.StartRunRequest.FederationConfigEntry\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\x1aK\n\x15\x46\x65\x64\x65rationConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"\"\n\x10StartRunResponse\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"#\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x12\"(\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t2\xa0\x01\n\x04\x45xec\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -26,18 +27,18 @@ _globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_options = b'8\001' _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._options = None _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._serialized_options = b'8\001' - _globals['_STARTRUNREQUEST']._serialized_start=66 - _globals['_STARTRUNREQUEST']._serialized_end=405 - _globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_start=255 - _globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_end=328 - _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._serialized_start=330 - _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._serialized_end=405 - _globals['_STARTRUNRESPONSE']._serialized_start=407 - _globals['_STARTRUNRESPONSE']._serialized_end=441 - _globals['_STREAMLOGSREQUEST']._serialized_start=443 - _globals['_STREAMLOGSREQUEST']._serialized_end=478 - _globals['_STREAMLOGSRESPONSE']._serialized_start=480 - _globals['_STREAMLOGSRESPONSE']._serialized_end=520 - _globals['_EXEC']._serialized_start=523 - _globals['_EXEC']._serialized_end=683 + _globals['_STARTRUNREQUEST']._serialized_start=88 + _globals['_STARTRUNREQUEST']._serialized_end=439 + _globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_start=289 + _globals['_STARTRUNREQUEST_OVERRIDECONFIGENTRY']._serialized_end=362 + _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._serialized_start=364 + _globals['_STARTRUNREQUEST_FEDERATIONCONFIGENTRY']._serialized_end=439 + _globals['_STARTRUNRESPONSE']._serialized_start=441 + _globals['_STARTRUNRESPONSE']._serialized_end=475 + _globals['_STREAMLOGSREQUEST']._serialized_start=477 + _globals['_STREAMLOGSREQUEST']._serialized_end=512 + _globals['_STREAMLOGSRESPONSE']._serialized_start=514 + _globals['_STREAMLOGSRESPONSE']._serialized_end=554 + _globals['_EXEC']._serialized_start=557 + _globals['_EXEC']._serialized_end=717 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/exec_pb2.pyi b/src/py/flwr/proto/exec_pb2.pyi index 79d54a90856b..8b7e07c8875f 100644 --- a/src/py/flwr/proto/exec_pb2.pyi +++ b/src/py/flwr/proto/exec_pb2.pyi @@ -3,6 +3,7 @@ isort:skip_file """ import builtins +import flwr.proto.fab_pb2 import flwr.proto.transport_pb2 import google.protobuf.descriptor import google.protobuf.internal.containers @@ -44,21 +45,23 @@ class StartRunRequest(google.protobuf.message.Message): def HasField(self, field_name: typing_extensions.Literal["value",b"value"]) -> builtins.bool: ... def ClearField(self, field_name: typing_extensions.Literal["key",b"key","value",b"value"]) -> None: ... - FAB_FILE_FIELD_NUMBER: builtins.int + FAB_FIELD_NUMBER: builtins.int OVERRIDE_CONFIG_FIELD_NUMBER: builtins.int FEDERATION_CONFIG_FIELD_NUMBER: builtins.int - fab_file: builtins.bytes + @property + def fab(self) -> flwr.proto.fab_pb2.Fab: ... @property def override_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... @property def federation_config(self) -> google.protobuf.internal.containers.MessageMap[typing.Text, flwr.proto.transport_pb2.Scalar]: ... def __init__(self, *, - fab_file: builtins.bytes = ..., + fab: typing.Optional[flwr.proto.fab_pb2.Fab] = ..., override_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., federation_config: typing.Optional[typing.Mapping[typing.Text, flwr.proto.transport_pb2.Scalar]] = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["fab_file",b"fab_file","federation_config",b"federation_config","override_config",b"override_config"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["fab",b"fab"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["fab",b"fab","federation_config",b"federation_config","override_config",b"override_config"]) -> None: ... global___StartRunRequest = StartRunRequest class StartRunResponse(google.protobuf.message.Message): diff --git a/src/py/flwr/superexec/exec_servicer.py b/src/py/flwr/superexec/exec_servicer.py index 83aac7bd5fd6..dda3e96994de 100644 --- a/src/py/flwr/superexec/exec_servicer.py +++ b/src/py/flwr/superexec/exec_servicer.py @@ -47,7 +47,7 @@ def StartRun( log(INFO, "ExecServicer.StartRun") run = self.executor.start_run( - request.fab_file, + request.fab.content, user_config_from_proto(request.override_config), user_config_from_proto(request.federation_config), ) diff --git a/src/py/flwr/superexec/exec_servicer_test.py b/src/py/flwr/superexec/exec_servicer_test.py index e55427572fd9..83717d63a36e 100644 --- a/src/py/flwr/superexec/exec_servicer_test.py +++ b/src/py/flwr/superexec/exec_servicer_test.py @@ -41,7 +41,7 @@ def test_start_run() -> None: context_mock = MagicMock() request = StartRunRequest() - request.fab_file = b"test" + request.fab.content = b"test" # Create a instance of FlowerServiceServicer servicer = ExecServicer(executor=executor) From 4e617a3731b6956687e9018e75f7311b7ba76dde Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Thu, 15 Aug 2024 13:01:07 +0100 Subject: [PATCH 065/188] refactor(framework:skip) Hide private `TypedDict` members and inherit from `MutableMapping` (#3218) --- src/py/flwr/common/record/recordset_test.py | 6 +- src/py/flwr/common/record/typeddict.py | 78 ++++++--------------- src/py/flwr/common/recordset_compat.py | 12 ++-- 3 files changed, 29 insertions(+), 67 deletions(-) diff --git a/src/py/flwr/common/record/recordset_test.py b/src/py/flwr/common/record/recordset_test.py index 01260793cb41..96556d335f4c 100644 --- a/src/py/flwr/common/record/recordset_test.py +++ b/src/py/flwr/common/record/recordset_test.py @@ -169,7 +169,7 @@ def test_set_parameters_with_incorrect_types( } with pytest.raises(TypeError): - p_record.update(array_dict) + p_record.update(array_dict) # type: ignore @pytest.mark.parametrize( @@ -250,7 +250,7 @@ def test_set_metrics_to_metricsrecord_with_incorrect_types( ) with pytest.raises(TypeError): - m_record.update(my_metrics) + m_record.update(my_metrics) # type: ignore @pytest.mark.parametrize( @@ -360,7 +360,7 @@ def test_set_configs_to_configsrecord_with_incorrect_types( ) with pytest.raises(TypeError): - c_record.update(my_configs) + c_record.update(my_configs) # type: ignore def test_count_bytes_metricsrecord() -> None: diff --git a/src/py/flwr/common/record/typeddict.py b/src/py/flwr/common/record/typeddict.py index 23d70dc4f7e8..791077d8eff2 100644 --- a/src/py/flwr/common/record/typeddict.py +++ b/src/py/flwr/common/record/typeddict.py @@ -15,99 +15,61 @@ """Typed dict base class for *Records.""" -from typing import Any, Callable, Dict, Generic, Iterator, Tuple, TypeVar, cast +from typing import Callable, Dict, Generic, Iterator, MutableMapping, TypeVar, cast K = TypeVar("K") # Key type V = TypeVar("V") # Value type -class TypedDict(Generic[K, V]): +class TypedDict(MutableMapping[K, V], Generic[K, V]): """Typed dictionary.""" def __init__( self, check_key_fn: Callable[[K], None], check_value_fn: Callable[[V], None] ): - self._data: Dict[K, V] = {} - self._check_key_fn = check_key_fn - self._check_value_fn = check_value_fn + self.__dict__["_check_key_fn"] = check_key_fn + self.__dict__["_check_value_fn"] = check_value_fn + self.__dict__["_data"] = {} def __setitem__(self, key: K, value: V) -> None: """Set the given key to the given value after type checking.""" # Check the types of key and value - self._check_key_fn(key) - self._check_value_fn(value) + cast(Callable[[K], None], self.__dict__["_check_key_fn"])(key) + cast(Callable[[V], None], self.__dict__["_check_value_fn"])(value) + # Set key-value pair - self._data[key] = value + cast(Dict[K, V], self.__dict__["_data"])[key] = value def __delitem__(self, key: K) -> None: """Remove the item with the specified key.""" - del self._data[key] + del cast(Dict[K, V], self.__dict__["_data"])[key] def __getitem__(self, item: K) -> V: """Return the value for the specified key.""" - return self._data[item] + return cast(Dict[K, V], self.__dict__["_data"])[item] def __iter__(self) -> Iterator[K]: """Yield an iterator over the keys of the dictionary.""" - return iter(self._data) + return iter(cast(Dict[K, V], self.__dict__["_data"])) def __repr__(self) -> str: """Return a string representation of the dictionary.""" - return self._data.__repr__() + return cast(Dict[K, V], self.__dict__["_data"]).__repr__() def __len__(self) -> int: """Return the number of items in the dictionary.""" - return len(self._data) + return len(cast(Dict[K, V], self.__dict__["_data"])) - def __contains__(self, key: K) -> bool: + def __contains__(self, key: object) -> bool: """Check if the dictionary contains the specified key.""" - return key in self._data + return key in cast(Dict[K, V], self.__dict__["_data"]) def __eq__(self, other: object) -> bool: """Compare this instance to another dictionary or TypedDict.""" + data = cast(Dict[K, V], self.__dict__["_data"]) if isinstance(other, TypedDict): - return self._data == other._data + other_data = cast(Dict[K, V], other.__dict__["_data"]) + return data == other_data if isinstance(other, dict): - return self._data == other + return data == other return NotImplemented - - def items(self) -> Iterator[Tuple[K, V]]: - """R.items() -> a set-like object providing a view on R's items.""" - return cast(Iterator[Tuple[K, V]], self._data.items()) - - def keys(self) -> Iterator[K]: - """R.keys() -> a set-like object providing a view on R's keys.""" - return cast(Iterator[K], self._data.keys()) - - def values(self) -> Iterator[V]: - """R.values() -> an object providing a view on R's values.""" - return cast(Iterator[V], self._data.values()) - - def update(self, *args: Any, **kwargs: Any) -> None: - """R.update([E, ]**F) -> None. - - Update R from dict/iterable E and F. - """ - for key, value in dict(*args, **kwargs).items(): - self[key] = value - - def pop(self, key: K) -> V: - """R.pop(k[,d]) -> v, remove specified key and return the corresponding value. - - If key is not found, d is returned if given, otherwise KeyError is raised. - """ - return self._data.pop(key) - - def get(self, key: K, default: V) -> V: - """R.get(k[,d]) -> R[k] if k in R, else d. - - d defaults to None. - """ - return self._data.get(key, default) - - def clear(self) -> None: - """R.clear() -> None. - - Remove all items from R. - """ - self._data.clear() diff --git a/src/py/flwr/common/recordset_compat.py b/src/py/flwr/common/recordset_compat.py index 1b0bf52d8277..8bf884c30e58 100644 --- a/src/py/flwr/common/recordset_compat.py +++ b/src/py/flwr/common/recordset_compat.py @@ -145,7 +145,7 @@ def _recordset_to_fit_or_evaluate_ins_components( # get config dict config_record = recordset.configs_records[f"{ins_str}.config"] # pylint: disable-next=protected-access - config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data) + config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record) return parameters, config_dict @@ -213,7 +213,7 @@ def recordset_to_fitres(recordset: RecordSet, keep_input: bool) -> FitRes: ) configs_record = recordset.configs_records[f"{ins_str}.metrics"] # pylint: disable-next=protected-access - metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record._data) + metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record) status = _extract_status_from_recordset(ins_str, recordset) return FitRes( @@ -274,7 +274,7 @@ def recordset_to_evaluateres(recordset: RecordSet) -> EvaluateRes: configs_record = recordset.configs_records[f"{ins_str}.metrics"] # pylint: disable-next=protected-access - metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record._data) + metrics = _check_mapping_from_recordscalartype_to_scalar(configs_record) status = _extract_status_from_recordset(ins_str, recordset) return EvaluateRes( @@ -314,7 +314,7 @@ def recordset_to_getparametersins(recordset: RecordSet) -> GetParametersIns: """Derive GetParametersIns from a RecordSet object.""" config_record = recordset.configs_records["getparametersins.config"] # pylint: disable-next=protected-access - config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data) + config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record) return GetParametersIns(config=config_dict) @@ -365,7 +365,7 @@ def recordset_to_getpropertiesins(recordset: RecordSet) -> GetPropertiesIns: """Derive GetPropertiesIns from a RecordSet object.""" config_record = recordset.configs_records["getpropertiesins.config"] # pylint: disable-next=protected-access - config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record._data) + config_dict = _check_mapping_from_recordscalartype_to_scalar(config_record) return GetPropertiesIns(config=config_dict) @@ -384,7 +384,7 @@ def recordset_to_getpropertiesres(recordset: RecordSet) -> GetPropertiesRes: res_str = "getpropertiesres" config_record = recordset.configs_records[f"{res_str}.properties"] # pylint: disable-next=protected-access - properties = _check_mapping_from_recordscalartype_to_scalar(config_record._data) + properties = _check_mapping_from_recordscalartype_to_scalar(config_record) status = _extract_status_from_recordset(res_str, recordset=recordset) From 5e707783182c23de6c3b05491bc10e8d4cf3f786 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Thu, 15 Aug 2024 14:23:09 +0100 Subject: [PATCH 066/188] refactor(framework:skip) Update annotations in `RecordSet` (#4018) --- src/py/flwr/common/record/recordset.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/common/record/recordset.py b/src/py/flwr/common/record/recordset.py index 74eed46ad86f..098b73b2d429 100644 --- a/src/py/flwr/common/record/recordset.py +++ b/src/py/flwr/common/record/recordset.py @@ -15,8 +15,10 @@ """RecordSet.""" +from __future__ import annotations + from dataclasses import dataclass -from typing import Dict, Optional, cast +from typing import cast from .configsrecord import ConfigsRecord from .metricsrecord import MetricsRecord @@ -34,9 +36,9 @@ class RecordSetData: def __init__( self, - parameters_records: Optional[Dict[str, ParametersRecord]] = None, - metrics_records: Optional[Dict[str, MetricsRecord]] = None, - configs_records: Optional[Dict[str, ConfigsRecord]] = None, + parameters_records: dict[str, ParametersRecord] | None = None, + metrics_records: dict[str, MetricsRecord] | None = None, + configs_records: dict[str, ConfigsRecord] | None = None, ) -> None: self.parameters_records = TypedDict[str, ParametersRecord]( self._check_fn_str, self._check_fn_params @@ -88,9 +90,9 @@ class RecordSet: def __init__( self, - parameters_records: Optional[Dict[str, ParametersRecord]] = None, - metrics_records: Optional[Dict[str, MetricsRecord]] = None, - configs_records: Optional[Dict[str, ConfigsRecord]] = None, + parameters_records: dict[str, ParametersRecord] | None = None, + metrics_records: dict[str, MetricsRecord] | None = None, + configs_records: dict[str, ConfigsRecord] | None = None, ) -> None: data = RecordSetData( parameters_records=parameters_records, From 480f683d66bdb54e88ba115623ab9bfad57a4ba8 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 17:35:27 +0200 Subject: [PATCH 067/188] feat(framework) Add `fab_hash` to Run (#4006) --- src/py/flwr/client/app.py | 2 +- .../client/grpc_rere_client/connection.py | 1 + src/py/flwr/client/rest_client/connection.py | 3 +- src/py/flwr/common/serde.py | 3 +- src/py/flwr/common/serde_test.py | 1 + src/py/flwr/common/typing.py | 1 + src/py/flwr/server/app.py | 12 +++++ src/py/flwr/server/driver/grpc_driver.py | 1 + .../server/driver/inmemory_driver_test.py | 6 ++- .../server/superlink/driver/driver_grpc.py | 3 ++ .../superlink/driver/driver_servicer.py | 15 +++++- .../grpc_rere/server_interceptor_test.py | 4 +- .../superlink/fleet/vce/vce_api_test.py | 8 ++- .../server/superlink/state/in_memory_state.py | 12 +++-- .../server/superlink/state/sqlite_state.py | 24 ++++++--- src/py/flwr/server/superlink/state/state.py | 7 +-- .../flwr/server/superlink/state/state_test.py | 49 +++++++++---------- src/py/flwr/simulation/run_simulation.py | 5 +- 18 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 7acd75353821..e20eee78e631 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -398,7 +398,7 @@ def _on_backoff(retry_state: RetryState) -> None: runs[run_id] = get_run(run_id) # If get_run is None, i.e., in grpc-bidi mode else: - runs[run_id] = Run(run_id, "", "", {}) + runs[run_id] = Run(run_id, "", "", "", {}) # Register context for this run node_state.register_context( diff --git a/src/py/flwr/client/grpc_rere_client/connection.py b/src/py/flwr/client/grpc_rere_client/connection.py index af74125140e9..155beb8a5630 100644 --- a/src/py/flwr/client/grpc_rere_client/connection.py +++ b/src/py/flwr/client/grpc_rere_client/connection.py @@ -286,6 +286,7 @@ def get_run(run_id: int) -> Run: run_id, get_run_response.run.fab_id, get_run_response.run.fab_version, + get_run_response.run.fab_hash, user_config_from_proto(get_run_response.run.override_config), ) diff --git a/src/py/flwr/client/rest_client/connection.py b/src/py/flwr/client/rest_client/connection.py index 2da320622c17..3f9147304fd7 100644 --- a/src/py/flwr/client/rest_client/connection.py +++ b/src/py/flwr/client/rest_client/connection.py @@ -358,12 +358,13 @@ def get_run(run_id: int) -> Run: # Send the request res = _request(req, GetRunResponse, PATH_GET_RUN) if res is None: - return Run(run_id, "", "", {}) + return Run(run_id, "", "", "", {}) return Run( run_id, res.run.fab_id, res.run.fab_version, + res.run.fab_hash, user_config_from_proto(res.run.override_config), ) diff --git a/src/py/flwr/common/serde.py b/src/py/flwr/common/serde.py index 819b113d041b..76265b9836d1 100644 --- a/src/py/flwr/common/serde.py +++ b/src/py/flwr/common/serde.py @@ -850,8 +850,8 @@ def run_to_proto(run: typing.Run) -> ProtoRun: run_id=run.run_id, fab_id=run.fab_id, fab_version=run.fab_version, + fab_hash=run.fab_hash, override_config=user_config_to_proto(run.override_config), - fab_hash="", ) return proto @@ -862,6 +862,7 @@ def run_from_proto(run_proto: ProtoRun) -> typing.Run: run_id=run_proto.run_id, fab_id=run_proto.fab_id, fab_version=run_proto.fab_version, + fab_hash=run_proto.fab_hash, override_config=user_config_from_proto(run_proto.override_config), ) return run diff --git a/src/py/flwr/common/serde_test.py b/src/py/flwr/common/serde_test.py index f3279c10e347..013d04a32fd4 100644 --- a/src/py/flwr/common/serde_test.py +++ b/src/py/flwr/common/serde_test.py @@ -519,6 +519,7 @@ def test_run_serialization_deserialization() -> None: run_id=1, fab_id="lorem", fab_version="ipsum", + fab_hash="hash", override_config=maker.user_config(), ) diff --git a/src/py/flwr/common/typing.py b/src/py/flwr/common/typing.py index 68a2b5825f06..b1dec8d0420b 100644 --- a/src/py/flwr/common/typing.py +++ b/src/py/flwr/common/typing.py @@ -214,6 +214,7 @@ class Run: run_id: int fab_id: str fab_version: str + fab_hash: str override_config: UserConfig diff --git a/src/py/flwr/server/app.py b/src/py/flwr/server/app.py index 822defdb5b13..fc60cb331b7f 100644 --- a/src/py/flwr/server/app.py +++ b/src/py/flwr/server/app.py @@ -34,6 +34,7 @@ from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, event from flwr.common.address import parse_address +from flwr.common.config import get_flwr_dir from flwr.common.constant import ( MISSING_EXTRA_REST, TRANSPORT_TYPE_GRPC_ADAPTER, @@ -57,6 +58,7 @@ from .server_config import ServerConfig from .strategy import Strategy from .superlink.driver.driver_grpc import run_driver_api_grpc +from .superlink.ffs.ffs_factory import FfsFactory from .superlink.fleet.grpc_adapter.grpc_adapter_servicer import GrpcAdapterServicer from .superlink.fleet.grpc_bidi.grpc_server import ( generic_create_grpc_server, @@ -72,6 +74,7 @@ ADDRESS_FLEET_API_REST = "0.0.0.0:9093" DATABASE = ":flwr-in-memory-state:" +BASE_DIR = get_flwr_dir() / "superlink" / "ffs" def start_server( # pylint: disable=too-many-arguments,too-many-locals @@ -211,10 +214,14 @@ def run_superlink() -> None: # Initialize StateFactory state_factory = StateFactory(args.database) + # Initialize FfsFactory + ffs_factory = FfsFactory(args.storage_dir) + # Start Driver API driver_server: grpc.Server = run_driver_api_grpc( address=driver_address, state_factory=state_factory, + ffs_factory=ffs_factory, certificates=certificates, ) @@ -610,6 +617,11 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None: "Flower will just create a state in memory.", default=DATABASE, ) + parser.add_argument( + "--storage-dir", + help="The base directory to store the objects for the Flower File System.", + default=BASE_DIR, + ) parser.add_argument( "--auth-list-public-keys", type=str, diff --git a/src/py/flwr/server/driver/grpc_driver.py b/src/py/flwr/server/driver/grpc_driver.py index 60439892d946..80ce9623ab3f 100644 --- a/src/py/flwr/server/driver/grpc_driver.py +++ b/src/py/flwr/server/driver/grpc_driver.py @@ -131,6 +131,7 @@ def _init_run(self) -> None: run_id=res.run.run_id, fab_id=res.run.fab_id, fab_version=res.run.fab_version, + fab_hash=res.run.fab_hash, override_config=user_config_from_proto(res.run.override_config), ) diff --git a/src/py/flwr/server/driver/inmemory_driver_test.py b/src/py/flwr/server/driver/inmemory_driver_test.py index d0f32e830f7d..610452e225be 100644 --- a/src/py/flwr/server/driver/inmemory_driver_test.py +++ b/src/py/flwr/server/driver/inmemory_driver_test.py @@ -89,6 +89,7 @@ def setUp(self) -> None: run_id=61016, fab_id="mock/mock", fab_version="v1.0.0", + fab_hash="hash", override_config={"test_key": "test_value"}, ) state_factory = MagicMock(state=lambda: self.state) @@ -101,6 +102,7 @@ def test_get_run(self) -> None: self.assertEqual(self.driver.run.run_id, 61016) self.assertEqual(self.driver.run.fab_id, "mock/mock") self.assertEqual(self.driver.run.fab_version, "v1.0.0") + self.assertEqual(self.driver.run.fab_hash, "hash") self.assertEqual(self.driver.run.override_config["test_key"], "test_value") def test_get_nodes(self) -> None: @@ -227,7 +229,7 @@ def test_task_store_consistency_after_push_pull_sqlitestate(self) -> None: # Prepare state = StateFactory("").state() self.driver = InMemoryDriver( - state.create_run("", "", {}), MagicMock(state=lambda: state) + state.create_run("", "", "", {}), MagicMock(state=lambda: state) ) msg_ids, node_id = push_messages(self.driver, self.num_nodes) assert isinstance(state, SqliteState) @@ -253,7 +255,7 @@ def test_task_store_consistency_after_push_pull_inmemory_state(self) -> None: # Prepare state_factory = StateFactory(":flwr-in-memory-state:") state = state_factory.state() - self.driver = InMemoryDriver(state.create_run("", "", {}), state_factory) + self.driver = InMemoryDriver(state.create_run("", "", "", {}), state_factory) msg_ids, node_id = push_messages(self.driver, self.num_nodes) assert isinstance(state, InMemoryState) diff --git a/src/py/flwr/server/superlink/driver/driver_grpc.py b/src/py/flwr/server/superlink/driver/driver_grpc.py index 782935481945..b7b914206f72 100644 --- a/src/py/flwr/server/superlink/driver/driver_grpc.py +++ b/src/py/flwr/server/superlink/driver/driver_grpc.py @@ -24,6 +24,7 @@ from flwr.proto.driver_pb2_grpc import ( # pylint: disable=E0611 add_DriverServicer_to_server, ) +from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.state import StateFactory from ..fleet.grpc_bidi.grpc_server import generic_create_grpc_server @@ -33,12 +34,14 @@ def run_driver_api_grpc( address: str, state_factory: StateFactory, + ffs_factory: FfsFactory, certificates: Optional[Tuple[bytes, bytes, bytes]], ) -> grpc.Server: """Run Driver API (gRPC, request-response).""" # Create Driver API gRPC server driver_servicer: grpc.Server = DriverServicer( state_factory=state_factory, + ffs_factory=ffs_factory, ) driver_add_servicer_to_server_fn = add_DriverServicer_to_server driver_grpc_server = generic_create_grpc_server( diff --git a/src/py/flwr/server/superlink/driver/driver_servicer.py b/src/py/flwr/server/superlink/driver/driver_servicer.py index 7819c587e850..8236f9b50d79 100644 --- a/src/py/flwr/server/superlink/driver/driver_servicer.py +++ b/src/py/flwr/server/superlink/driver/driver_servicer.py @@ -43,6 +43,8 @@ Run, ) from flwr.proto.task_pb2 import TaskRes # pylint: disable=E0611 +from flwr.server.superlink.ffs import Ffs +from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.state import State, StateFactory from flwr.server.utils.validator import validate_task_ins_or_res @@ -50,8 +52,9 @@ class DriverServicer(driver_pb2_grpc.DriverServicer): """Driver API servicer.""" - def __init__(self, state_factory: StateFactory) -> None: + def __init__(self, state_factory: StateFactory, ffs_factory: FfsFactory) -> None: self.state_factory = state_factory + self.ffs_factory = ffs_factory def GetNodes( self, request: GetNodesRequest, context: grpc.ServicerContext @@ -71,9 +74,19 @@ def CreateRun( """Create run ID.""" log(DEBUG, "DriverServicer.CreateRun") state: State = self.state_factory.state() + if request.HasField("fab") and request.fab.HasField("content"): + ffs: Ffs = self.ffs_factory.ffs() + fab_hash = ffs.put(request.fab.content, {}) + _raise_if( + fab_hash != request.fab.hash_str, + f"FAB ({request.fab}) hash from request doesn't match contents", + ) + else: + fab_hash = "" run_id = state.create_run( request.fab_id, request.fab_version, + fab_hash, user_config_from_proto(request.override_config), ) return CreateRunResponse(run_id=run_id) diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py index 798e71435585..b87a4293a775 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py @@ -328,7 +328,7 @@ def test_successful_get_run_with_metadata(self) -> None: self.state.create_node( ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) ) - run_id = self.state.create_run("", "", {}) + run_id = self.state.create_run("", "", "", {}) request = GetRunRequest(run_id=run_id) shared_secret = generate_shared_key( self._client_private_key, self._server_public_key @@ -359,7 +359,7 @@ def test_unsuccessful_get_run_with_metadata(self) -> None: self.state.create_node( ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) ) - run_id = self.state.create_run("", "", {}) + run_id = self.state.create_run("", "", "", {}) request = GetRunRequest(run_id=run_id) client_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(client_private_key, self._server_public_key) diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py index 70c8669ad883..28ed23cf6509 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py @@ -109,7 +109,11 @@ def register_messages_into_state( """Register `num_messages` into the state factory.""" state: InMemoryState = state_factory.state() # type: ignore state.run_ids[run_id] = Run( - run_id=run_id, fab_id="Mock/mock", fab_version="v1.0.0", override_config={} + run_id=run_id, + fab_id="Mock/mock", + fab_version="v1.0.0", + fab_hash="hash", + override_config={}, ) # Artificially add TaskIns to state so they can be processed # by the Simulation Engine logic @@ -192,7 +196,7 @@ def start_and_shutdown( if not app_dir: app_dir = _autoresolve_app_dir() - run = Run(run_id=1234, fab_id="", fab_version="", override_config={}) + run = Run(run_id=1234, fab_id="", fab_version="", fab_hash="", override_config={}) start_vce( num_supernodes=num_supernodes, diff --git a/src/py/flwr/server/superlink/state/in_memory_state.py b/src/py/flwr/server/superlink/state/in_memory_state.py index beb25ba4e84f..fde8fe41912f 100644 --- a/src/py/flwr/server/superlink/state/in_memory_state.py +++ b/src/py/flwr/server/superlink/state/in_memory_state.py @@ -277,11 +277,12 @@ def get_node_id(self, client_public_key: bytes) -> Optional[int]: def create_run( self, - fab_id: str, - fab_version: str, + fab_id: Optional[str], + fab_version: Optional[str], + fab_hash: Optional[str], override_config: UserConfig, ) -> int: - """Create a new run for the specified `fab_id` and `fab_version`.""" + """Create a new run for the specified `fab_hash`.""" # Sample a random int64 as run_id with self.lock: run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) @@ -289,8 +290,9 @@ def create_run( if run_id not in self.run_ids: self.run_ids[run_id] = Run( run_id=run_id, - fab_id=fab_id, - fab_version=fab_version, + fab_id=fab_id if fab_id else "", + fab_version=fab_version if fab_version else "", + fab_hash=fab_hash if fab_hash else "", override_config=override_config, ) return run_id diff --git a/src/py/flwr/server/superlink/state/sqlite_state.py b/src/py/flwr/server/superlink/state/sqlite_state.py index bd3b6ebabd83..93b3cd63ca7f 100644 --- a/src/py/flwr/server/superlink/state/sqlite_state.py +++ b/src/py/flwr/server/superlink/state/sqlite_state.py @@ -65,6 +65,7 @@ run_id INTEGER UNIQUE, fab_id TEXT, fab_version TEXT, + fab_hash TEXT, override_config TEXT ); """ @@ -617,8 +618,9 @@ def get_node_id(self, client_public_key: bytes) -> Optional[int]: def create_run( self, - fab_id: str, - fab_version: str, + fab_id: Optional[str], + fab_version: Optional[str], + fab_hash: Optional[str], override_config: UserConfig, ) -> int: """Create a new run for the specified `fab_id` and `fab_version`.""" @@ -630,12 +632,19 @@ def create_run( # If run_id does not exist if self.query(query, (run_id,))[0]["COUNT(*)"] == 0: query = ( - "INSERT INTO run (run_id, fab_id, fab_version, override_config)" - "VALUES (?, ?, ?, ?);" - ) - self.query( - query, (run_id, fab_id, fab_version, json.dumps(override_config)) + "INSERT INTO run " + "(run_id, fab_id, fab_version, fab_hash, override_config)" + "VALUES (?, ?, ?, ?, ?);" ) + if fab_hash: + self.query( + query, (run_id, "", "", fab_hash, json.dumps(override_config)) + ) + else: + self.query( + query, + (run_id, fab_id, fab_version, "", json.dumps(override_config)), + ) return run_id log(ERROR, "Unexpected run creation failure.") return 0 @@ -702,6 +711,7 @@ def get_run(self, run_id: int) -> Optional[Run]: run_id=run_id, fab_id=row["fab_id"], fab_version=row["fab_version"], + fab_hash=row["fab_hash"], override_config=json.loads(row["override_config"]), ) except sqlite3.IntegrityError: diff --git a/src/py/flwr/server/superlink/state/state.py b/src/py/flwr/server/superlink/state/state.py index 23c95805948e..80d3b799bce3 100644 --- a/src/py/flwr/server/superlink/state/state.py +++ b/src/py/flwr/server/superlink/state/state.py @@ -159,11 +159,12 @@ def get_node_id(self, client_public_key: bytes) -> Optional[int]: @abc.abstractmethod def create_run( self, - fab_id: str, - fab_version: str, + fab_id: Optional[str], + fab_version: Optional[str], + fab_hash: Optional[str], override_config: UserConfig, ) -> int: - """Create a new run for the specified `fab_id` and `fab_version`.""" + """Create a new run for the specified `fab_hash`.""" @abc.abstractmethod def get_run(self, run_id: int) -> Optional[Run]: diff --git a/src/py/flwr/server/superlink/state/state_test.py b/src/py/flwr/server/superlink/state/state_test.py index 5f0d23ffc4d8..3efce9ca0c88 100644 --- a/src/py/flwr/server/superlink/state/state_test.py +++ b/src/py/flwr/server/superlink/state/state_test.py @@ -52,7 +52,7 @@ def test_create_and_get_run(self) -> None: """Test if create_run and get_run work correctly.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("Mock/mock", "v1.0.0", {"test_key": "test_value"}) + run_id = state.create_run(None, None, "9f86d08", {"test_key": "test_value"}) # Execute run = state.get_run(run_id) @@ -60,8 +60,7 @@ def test_create_and_get_run(self) -> None: # Assert assert run is not None assert run.run_id == run_id - assert run.fab_id == "Mock/mock" - assert run.fab_version == "v1.0.0" + assert run.fab_hash == "9f86d08" assert run.override_config["test_key"] == "test_value" def test_get_task_ins_empty(self) -> None: @@ -91,7 +90,7 @@ def test_store_task_ins_one(self) -> None: # Prepare consumer_node_id = 1 state = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins( consumer_node_id=consumer_node_id, anonymous=False, run_id=run_id ) @@ -126,7 +125,7 @@ def test_store_and_delete_tasks(self) -> None: # Prepare consumer_node_id = 1 state = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins_0 = create_task_ins( consumer_node_id=consumer_node_id, anonymous=False, run_id=run_id ) @@ -200,7 +199,7 @@ def test_task_ins_store_anonymous_and_retrieve_anonymous(self) -> None: """ # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins(consumer_node_id=0, anonymous=True, run_id=run_id) # Execute @@ -215,7 +214,7 @@ def test_task_ins_store_anonymous_and_fail_retrieving_identitiy(self) -> None: """Store anonymous TaskIns and fail to retrieve it.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins(consumer_node_id=0, anonymous=True, run_id=run_id) # Execute @@ -229,7 +228,7 @@ def test_task_ins_store_identity_and_fail_retrieving_anonymous(self) -> None: """Store identity TaskIns and fail retrieving it as anonymous.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins(consumer_node_id=1, anonymous=False, run_id=run_id) # Execute @@ -243,7 +242,7 @@ def test_task_ins_store_identity_and_retrieve_identity(self) -> None: """Store identity TaskIns and retrieve it.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins(consumer_node_id=1, anonymous=False, run_id=run_id) # Execute @@ -260,7 +259,7 @@ def test_task_ins_store_delivered_and_fail_retrieving(self) -> None: """Fail retrieving delivered task.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins = create_task_ins(consumer_node_id=1, anonymous=False, run_id=run_id) # Execute @@ -303,7 +302,7 @@ def test_task_res_store_and_retrieve_by_task_ins_id(self) -> None: """Store TaskRes retrieve it by task_ins_id.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_ins_id = uuid4() task_res = create_task_res( producer_node_id=0, @@ -324,7 +323,7 @@ def test_node_ids_initial_state(self) -> None: """Test retrieving all node_ids and empty initial state.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) # Execute retrieved_node_ids = state.get_nodes(run_id) @@ -336,7 +335,7 @@ def test_create_node_and_get_nodes(self) -> None: """Test creating a client node.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_ids = [] # Execute @@ -353,7 +352,7 @@ def test_create_node_public_key(self) -> None: # Prepare state: State = self.state_factory() public_key = b"mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) # Execute node_id = state.create_node(ping_interval=10, public_key=public_key) @@ -369,7 +368,7 @@ def test_create_node_public_key_twice(self) -> None: # Prepare state: State = self.state_factory() public_key = b"mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id = state.create_node(ping_interval=10, public_key=public_key) # Execute @@ -391,7 +390,7 @@ def test_delete_node(self) -> None: """Test deleting a client node.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id = state.create_node(ping_interval=10) # Execute @@ -406,7 +405,7 @@ def test_delete_node_public_key(self) -> None: # Prepare state: State = self.state_factory() public_key = b"mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id = state.create_node(ping_interval=10, public_key=public_key) # Execute @@ -423,7 +422,7 @@ def test_delete_node_public_key_none(self) -> None: # Prepare state: State = self.state_factory() public_key = b"mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id = 0 # Execute & Assert @@ -442,7 +441,7 @@ def test_delete_node_wrong_public_key(self) -> None: state: State = self.state_factory() public_key = b"mock" wrong_public_key = b"mock_mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id = state.create_node(ping_interval=10, public_key=public_key) # Execute & Assert @@ -461,7 +460,7 @@ def test_get_node_id_wrong_public_key(self) -> None: state: State = self.state_factory() public_key = b"mock" wrong_public_key = b"mock_mock" - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) # Execute state.create_node(ping_interval=10, public_key=public_key) @@ -476,7 +475,7 @@ def test_get_nodes_invalid_run_id(self) -> None: """Test retrieving all node_ids with invalid run_id.""" # Prepare state: State = self.state_factory() - state.create_run("mock/mock", "v1.0.0", {}) + state.create_run(None, None, "9f86d08", {}) invalid_run_id = 61016 state.create_node(ping_interval=10) @@ -490,7 +489,7 @@ def test_num_task_ins(self) -> None: """Test if num_tasks returns correct number of not delivered task_ins.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_0 = create_task_ins(consumer_node_id=0, anonymous=True, run_id=run_id) task_1 = create_task_ins(consumer_node_id=0, anonymous=True, run_id=run_id) @@ -508,7 +507,7 @@ def test_num_task_res(self) -> None: """Test if num_tasks returns correct number of not delivered task_res.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) task_0 = create_task_res( producer_node_id=0, anonymous=True, ancestry=["1"], run_id=run_id ) @@ -609,7 +608,7 @@ def test_acknowledge_ping(self) -> None: """Test if acknowledge_ping works and if get_nodes return online nodes.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_ids = [state.create_node(ping_interval=10) for _ in range(100)] for node_id in node_ids[:70]: state.acknowledge_ping(node_id, ping_interval=30) @@ -628,7 +627,7 @@ def test_node_unavailable_error(self) -> None: """Test if get_task_res return TaskRes containing node unavailable error.""" # Prepare state: State = self.state_factory() - run_id = state.create_run("mock/mock", "v1.0.0", {}) + run_id = state.create_run(None, None, "9f86d08", {}) node_id_0 = state.create_node(ping_interval=90) node_id_1 = state.create_node(ping_interval=30) # Create and store TaskIns diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 51799074ef6f..257a066433fa 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -163,6 +163,7 @@ def run_simulation_from_cli() -> None: run_id=run_id, fab_id="", fab_version="", + fab_hash="", override_config=override_config, ) @@ -529,7 +530,9 @@ def _run_simulation( # If no `Run` object is set, create one if run is None: run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) - run = Run(run_id=run_id, fab_id="", fab_version="", override_config={}) + run = Run( + run_id=run_id, fab_id="", fab_version="", fab_hash="", override_config={} + ) args = ( num_supernodes, From 9c85be5a6c932eb7ec8166f1b2a58b5b386a92e5 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Thu, 15 Aug 2024 16:47:51 +0100 Subject: [PATCH 068/188] feat(framework) Add `ClientAppIo` servicer (#3976) --- src/py/flwr/client/process/__init__.py | 15 ++ .../client/process/clientappio_servicer.py | 145 ++++++++++++++++++ .../process/clientappio_servicer_test.py | 118 ++++++++++++++ 3 files changed, 278 insertions(+) create mode 100644 src/py/flwr/client/process/__init__.py create mode 100644 src/py/flwr/client/process/clientappio_servicer.py create mode 100644 src/py/flwr/client/process/clientappio_servicer_test.py diff --git a/src/py/flwr/client/process/__init__.py b/src/py/flwr/client/process/__init__.py new file mode 100644 index 000000000000..653cee434c12 --- /dev/null +++ b/src/py/flwr/client/process/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower AppIO service.""" diff --git a/src/py/flwr/client/process/clientappio_servicer.py b/src/py/flwr/client/process/clientappio_servicer.py new file mode 100644 index 000000000000..f614fadf8070 --- /dev/null +++ b/src/py/flwr/client/process/clientappio_servicer.py @@ -0,0 +1,145 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""ClientAppIo API servicer.""" + + +from dataclasses import dataclass +from logging import DEBUG, ERROR +from typing import Optional + +import grpc + +from flwr.common import Context, Message, typing +from flwr.common.logger import log +from flwr.common.serde import ( + clientappstatus_to_proto, + context_from_proto, + context_to_proto, + message_from_proto, + message_to_proto, + run_to_proto, +) +from flwr.common.typing import Run + +# pylint: disable=E0611 +from flwr.proto import clientappio_pb2_grpc +from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0401 + PullClientAppInputsRequest, + PullClientAppInputsResponse, + PushClientAppOutputsRequest, + PushClientAppOutputsResponse, +) + + +@dataclass +class ClientAppIoInputs: + """Specify the inputs to the ClientApp.""" + + message: Message + context: Context + run: Run + token: int + + +@dataclass +class ClientAppIoOutputs: + """Specify the outputs from the ClientApp.""" + + message: Message + context: Context + + +# pylint: disable=C0103,W0613,W0201 +class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer): + """ClientAppIo API servicer.""" + + def __init__(self) -> None: + self.clientapp_input: Optional[ClientAppIoInputs] = None + self.clientapp_output: Optional[ClientAppIoOutputs] = None + + def PullClientAppInputs( + self, request: PullClientAppInputsRequest, context: grpc.ServicerContext + ) -> PullClientAppInputsResponse: + """Pull Message, Context, and Run.""" + log(DEBUG, "ClientAppIo.PullClientAppInputs") + if self.clientapp_input is None: + raise ValueError( + "ClientAppIoInputs not set before calling `PullClientAppInputs`." + ) + if request.token != self.clientapp_input.token: + context.abort( + grpc.StatusCode.INVALID_ARGUMENT, + "Mismatch between ClientApp and SuperNode token", + ) + return PullClientAppInputsResponse( + message=message_to_proto(self.clientapp_input.message), + context=context_to_proto(self.clientapp_input.context), + run=run_to_proto(self.clientapp_input.run), + ) + + def PushClientAppOutputs( + self, request: PushClientAppOutputsRequest, context: grpc.ServicerContext + ) -> PushClientAppOutputsResponse: + """Push Message and Context.""" + log(DEBUG, "ClientAppIo.PushClientAppOutputs") + if self.clientapp_output is None: + raise ValueError( + "ClientAppIoOutputs not set before calling `PushClientAppOutputs`." + ) + if self.clientapp_input is None: + raise ValueError( + "ClientAppIoInputs not set before calling `PushClientAppOutputs`." + ) + if request.token != self.clientapp_input.token: + context.abort( + grpc.StatusCode.INVALID_ARGUMENT, + "Mismatch between ClientApp and SuperNode token", + ) + try: + # Update Message and Context + self.clientapp_output.message = message_from_proto(request.message) + self.clientapp_output.context = context_from_proto(request.context) + # Set status + code = typing.ClientAppOutputCode.SUCCESS + status = typing.ClientAppOutputStatus(code=code, message="Success") + proto_status = clientappstatus_to_proto(status=status) + return PushClientAppOutputsResponse(status=proto_status) + except Exception as e: # pylint: disable=broad-exception-caught + log(ERROR, "ClientApp failed to push message to SuperNode, %s", e) + code = typing.ClientAppOutputCode.UNKNOWN_ERROR + status = typing.ClientAppOutputStatus(code=code, message="Push failed") + proto_status = clientappstatus_to_proto(status=status) + return PushClientAppOutputsResponse(status=proto_status) + + def set_inputs(self, clientapp_input: ClientAppIoInputs) -> None: + """Set ClientApp inputs.""" + log(DEBUG, "ClientAppIo.SetInputs") + if self.clientapp_input is not None or self.clientapp_output is not None: + raise ValueError( + "ClientAppIoInputs and ClientAppIoOutputs must not be set before " + "calling `set_inputs`." + ) + self.clientapp_input = clientapp_input + + def get_outputs(self) -> ClientAppIoOutputs: + """Get ClientApp outputs.""" + log(DEBUG, "ClientAppIo.GetOutputs") + if self.clientapp_output is None: + raise ValueError("ClientAppIoOutputs not set before calling `get_outputs`.") + # Set outputs to a local variable and clear self.clientapp_output + output: ClientAppIoOutputs = self.clientapp_output + self.clientapp_input = None + self.clientapp_output = None + return output diff --git a/src/py/flwr/client/process/clientappio_servicer_test.py b/src/py/flwr/client/process/clientappio_servicer_test.py new file mode 100644 index 000000000000..b06e9eb0e1c0 --- /dev/null +++ b/src/py/flwr/client/process/clientappio_servicer_test.py @@ -0,0 +1,118 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Test the ClientAppIo API servicer.""" + +import unittest + +from flwr.common import Context, Message, typing +from flwr.common.serde_test import RecordMaker + +from .clientappio_servicer import ( + ClientAppIoInputs, + ClientAppIoOutputs, + ClientAppIoServicer, +) + + +class TestClientAppIoServicer(unittest.TestCase): + """Tests for `ClientAppIoServicer` class.""" + + def setUp(self) -> None: + """Initialize.""" + self.servicer = ClientAppIoServicer() + self.maker = RecordMaker() + + def tearDown(self) -> None: + """Cleanup.""" + + def test_set_inputs(self) -> None: + """Test setting ClientApp inputs.""" + # Prepare + message = Message( + metadata=self.maker.metadata(), + content=self.maker.recordset(2, 2, 1), + ) + context = Context( + node_id=1, + node_config={"nodeconfig1": 4.2}, + state=self.maker.recordset(2, 2, 1), + run_config={"runconfig1": 6.1}, + ) + run = typing.Run( + run_id=1, + fab_id="lorem", + fab_version="ipsum", + fab_hash="dolor", + override_config=self.maker.user_config(), + ) + client_input = ClientAppIoInputs(message, context, run, 1) + client_output = ClientAppIoOutputs(message, context) + + # Execute and assert + # - when ClientAppIoInputs is not None, ClientAppIoOutputs is None + with self.assertRaises(ValueError): + self.servicer.clientapp_input = client_input + self.servicer.clientapp_output = None + self.servicer.set_inputs(client_input) + + # Execute and assert + # - when ClientAppIoInputs is None, ClientAppIoOutputs is not None + with self.assertRaises(ValueError): + self.servicer.clientapp_input = None + self.servicer.clientapp_output = client_output + self.servicer.set_inputs(client_input) + + # Execute and assert + # - when ClientAppIoInputs and ClientAppIoOutputs is not None + with self.assertRaises(ValueError): + self.servicer.clientapp_input = client_input + self.servicer.clientapp_output = client_output + self.servicer.set_inputs(client_input) + + # Execute and assert + # - when ClientAppIoInputs is set at .clientapp_input + self.servicer.clientapp_input = None + self.servicer.clientapp_output = None + self.servicer.set_inputs(client_input) + assert client_input == self.servicer.clientapp_input + + def test_get_outputs(self) -> None: + """Test getting ClientApp outputs.""" + # Prepare + message = Message( + metadata=self.maker.metadata(), + content=self.maker.recordset(2, 2, 1), + ) + context = Context( + node_id=1, + node_config={"nodeconfig1": 4.2}, + state=self.maker.recordset(2, 2, 1), + run_config={"runconfig1": 6.1}, + ) + client_output = ClientAppIoOutputs(message, context) + + # Execute and assert - when `ClientAppIoOutputs` is None + self.servicer.clientapp_output = None + with self.assertRaises(ValueError): + # `ClientAppIoOutputs` should not be None + _ = self.servicer.get_outputs() + + # Execute and assert - when `ClientAppIoOutputs` is not None + self.servicer.clientapp_output = client_output + output = self.servicer.get_outputs() + assert isinstance(output, ClientAppIoOutputs) + assert output == client_output + assert self.servicer.clientapp_input is None + assert self.servicer.clientapp_output is None From ead952b7a2a8218d2c70801cbc7977fe088dbf4c Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Thu, 15 Aug 2024 23:47:42 +0200 Subject: [PATCH 069/188] ci(framework) Extend tests for `fab_hash` (#4020) --- src/py/flwr/server/driver/grpc_driver_test.py | 8 +++++++- src/py/flwr/server/driver/inmemory_driver_test.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/server/driver/grpc_driver_test.py b/src/py/flwr/server/driver/grpc_driver_test.py index fdf3c676190d..20017126927d 100644 --- a/src/py/flwr/server/driver/grpc_driver_test.py +++ b/src/py/flwr/server/driver/grpc_driver_test.py @@ -39,7 +39,12 @@ class TestGrpcDriver(unittest.TestCase): def setUp(self) -> None: """Initialize mock GrpcDriverStub and Driver instance before each test.""" mock_response = Mock( - run=Run(run_id=61016, fab_id="mock/mock", fab_version="v1.0.0") + run=Run( + run_id=61016, + fab_id="mock/mock", + fab_version="v1.0.0", + fab_hash="9f86d08", + ) ) self.mock_stub = Mock() self.mock_channel = Mock() @@ -55,6 +60,7 @@ def test_init_grpc_driver(self) -> None: self.assertEqual(self.driver.run.run_id, 61016) self.assertEqual(self.driver.run.fab_id, "mock/mock") self.assertEqual(self.driver.run.fab_version, "v1.0.0") + self.assertEqual(self.driver.run.fab_hash, "9f86d08") self.mock_stub.GetRun.assert_called_once() def test_get_nodes(self) -> None: diff --git a/src/py/flwr/server/driver/inmemory_driver_test.py b/src/py/flwr/server/driver/inmemory_driver_test.py index 610452e225be..ddfdb249c1b4 100644 --- a/src/py/flwr/server/driver/inmemory_driver_test.py +++ b/src/py/flwr/server/driver/inmemory_driver_test.py @@ -89,7 +89,7 @@ def setUp(self) -> None: run_id=61016, fab_id="mock/mock", fab_version="v1.0.0", - fab_hash="hash", + fab_hash="9f86d08", override_config={"test_key": "test_value"}, ) state_factory = MagicMock(state=lambda: self.state) @@ -102,7 +102,7 @@ def test_get_run(self) -> None: self.assertEqual(self.driver.run.run_id, 61016) self.assertEqual(self.driver.run.fab_id, "mock/mock") self.assertEqual(self.driver.run.fab_version, "v1.0.0") - self.assertEqual(self.driver.run.fab_hash, "hash") + self.assertEqual(self.driver.run.fab_hash, "9f86d08") self.assertEqual(self.driver.run.override_config["test_key"], "test_value") def test_get_nodes(self) -> None: From 3c2c267205fb5476bc4c373cf15d7b8f90a3e0f9 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 16 Aug 2024 12:09:23 +0200 Subject: [PATCH 070/188] refactor(framework) Make `ffs.get` return optional (#4027) --- src/py/flwr/server/superlink/ffs/disk_ffs.py | 9 ++++++--- src/py/flwr/server/superlink/ffs/ffs.py | 6 +++--- src/py/flwr/server/superlink/ffs/ffs_test.py | 5 ++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/server/superlink/ffs/disk_ffs.py b/src/py/flwr/server/superlink/ffs/disk_ffs.py index 5331af500464..98ec4f93498f 100644 --- a/src/py/flwr/server/superlink/ffs/disk_ffs.py +++ b/src/py/flwr/server/superlink/ffs/disk_ffs.py @@ -17,7 +17,7 @@ import hashlib import json from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from flwr.server.superlink.ffs.ffs import Ffs @@ -58,7 +58,7 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: return content_hash - def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + def get(self, key: str) -> Optional[Tuple[bytes, Dict[str, str]]]: """Return tuple containing the object content and metadata. Parameters @@ -68,9 +68,12 @@ def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: Returns ------- - Tuple[bytes, Dict[str, str]] + Optional[Tuple[bytes, Dict[str, str]]] A tuple containing the object content and metadata. """ + if not (self.base_dir / key).exists(): + return None + content = (self.base_dir / key).read_bytes() meta = json.loads((self.base_dir / f"{key}.META").read_text()) diff --git a/src/py/flwr/server/superlink/ffs/ffs.py b/src/py/flwr/server/superlink/ffs/ffs.py index 622988141c9d..fab3b1fdfb3e 100644 --- a/src/py/flwr/server/superlink/ffs/ffs.py +++ b/src/py/flwr/server/superlink/ffs/ffs.py @@ -16,7 +16,7 @@ import abc -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple class Ffs(abc.ABC): # pylint: disable=R0904 @@ -40,7 +40,7 @@ def put(self, content: bytes, meta: Dict[str, str]) -> str: """ @abc.abstractmethod - def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: + def get(self, key: str) -> Optional[Tuple[bytes, Dict[str, str]]]: """Return tuple containing the object content and metadata. Parameters @@ -50,7 +50,7 @@ def get(self, key: str) -> Tuple[bytes, Dict[str, str]]: Returns ------- - Tuple[bytes, Dict[str, str]] + Optional[Tuple[bytes, Dict[str, str]]] A tuple containing the object content and metadata. """ diff --git a/src/py/flwr/server/superlink/ffs/ffs_test.py b/src/py/flwr/server/superlink/ffs/ffs_test.py index 3b25ac7b206a..f7fbbf1218e1 100644 --- a/src/py/flwr/server/superlink/ffs/ffs_test.py +++ b/src/py/flwr/server/superlink/ffs/ffs_test.py @@ -78,7 +78,10 @@ def test_get(self) -> None: json.dump(meta_expected, file) # Execute - content_actual, meta_actual = ffs.get(hash_expected) + result = ffs.get(hash_expected) + assert result is not None + + content_actual, meta_actual = result # Assert assert content_actual == content_expected From 755417c9da942d90b54e55deaacc4132b61412e5 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 16 Aug 2024 12:49:11 +0200 Subject: [PATCH 071/188] refactor(framework) Add `get_fab` implementations for FAB delivery (#4019) Co-authored-by: Daniel J. Beutel --- .../flwr/client/grpc_rere_client/connection.py | 9 ++++++++- src/py/flwr/client/rest_client/connection.py | 16 ++++++++++++++-- src/py/flwr/server/app.py | 3 +++ .../server/superlink/driver/driver_servicer.py | 17 +++++++++++++---- .../superlink/fleet/grpc_rere/fleet_servicer.py | 12 +++++++++--- .../fleet/grpc_rere/server_interceptor_test.py | 9 ++++++++- .../fleet/message_handler/message_handler.py | 17 ++++++++++++++++- 7 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/py/flwr/client/grpc_rere_client/connection.py b/src/py/flwr/client/grpc_rere_client/connection.py index 155beb8a5630..8bae253c819a 100644 --- a/src/py/flwr/client/grpc_rere_client/connection.py +++ b/src/py/flwr/client/grpc_rere_client/connection.py @@ -46,6 +46,7 @@ user_config_from_proto, ) from flwr.common.typing import Fab, Run +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, DeleteNodeRequest, @@ -292,7 +293,13 @@ def get_run(run_id: int) -> Run: def get_fab(fab_hash: str) -> Fab: # Call FleetAPI - raise NotImplementedError + get_fab_request = GetFabRequest(hash_str=fab_hash) + get_fab_response: GetFabResponse = retry_invoker.invoke( + stub.GetFab, + request=get_fab_request, + ) + + return Fab(get_fab_response.fab.hash_str, get_fab_response.fab.content) try: # Yield methods diff --git a/src/py/flwr/client/rest_client/connection.py b/src/py/flwr/client/rest_client/connection.py index 3f9147304fd7..8e5766bbc491 100644 --- a/src/py/flwr/client/rest_client/connection.py +++ b/src/py/flwr/client/rest_client/connection.py @@ -46,6 +46,7 @@ user_config_from_proto, ) from flwr.common.typing import Fab, Run +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -74,6 +75,7 @@ PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res" PATH_PING: str = "api/v0/fleet/ping" PATH_GET_RUN: str = "/api/v0/fleet/get-run" +PATH_GET_FAB: str = "/api/v0/fleet/get-fab" T = TypeVar("T", bound=GrpcMessage) @@ -369,8 +371,18 @@ def get_run(run_id: int) -> Run: ) def get_fab(fab_hash: str) -> Fab: - # Call FleetAPI - raise NotImplementedError + # Construct the request + req = GetFabRequest(hash_str=fab_hash) + + # Send the request + res = _request(req, GetFabResponse, PATH_GET_FAB) + if res is None: + return Fab("", b"") + + return Fab( + res.fab.hash_str, + res.fab.content, + ) try: # Yield methods diff --git a/src/py/flwr/server/app.py b/src/py/flwr/server/app.py index fc60cb331b7f..3a7a135ee6fb 100644 --- a/src/py/flwr/server/app.py +++ b/src/py/flwr/server/app.py @@ -301,6 +301,7 @@ def run_superlink() -> None: fleet_server = _run_fleet_api_grpc_rere( address=fleet_address, state_factory=state_factory, + ffs_factory=ffs_factory, certificates=certificates, interceptors=interceptors, ) @@ -487,6 +488,7 @@ def _try_obtain_certificates( def _run_fleet_api_grpc_rere( address: str, state_factory: StateFactory, + ffs_factory: FfsFactory, certificates: Optional[Tuple[bytes, bytes, bytes]], interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None, ) -> grpc.Server: @@ -494,6 +496,7 @@ def _run_fleet_api_grpc_rere( # Create Fleet API gRPC server fleet_servicer = FleetServicer( state_factory=state_factory, + ffs_factory=ffs_factory, ) fleet_add_servicer_to_server_fn = add_FleetServicer_to_server fleet_grpc_server = generic_create_grpc_server( diff --git a/src/py/flwr/server/superlink/driver/driver_servicer.py b/src/py/flwr/server/superlink/driver/driver_servicer.py index 8236f9b50d79..14e88d08da5b 100644 --- a/src/py/flwr/server/superlink/driver/driver_servicer.py +++ b/src/py/flwr/server/superlink/driver/driver_servicer.py @@ -23,7 +23,8 @@ import grpc from flwr.common.logger import log -from flwr.common.serde import user_config_from_proto, user_config_to_proto +from flwr.common.serde import fab_to_proto, user_config_from_proto, user_config_to_proto +from flwr.common.typing import Fab from flwr.proto import driver_pb2_grpc # pylint: disable=E0611 from flwr.proto.driver_pb2 import ( # pylint: disable=E0611 CreateRunRequest, @@ -43,7 +44,7 @@ Run, ) from flwr.proto.task_pb2 import TaskRes # pylint: disable=E0611 -from flwr.server.superlink.ffs import Ffs +from flwr.server.superlink.ffs.ffs import Ffs from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.state import State, StateFactory from flwr.server.utils.validator import validate_task_ins_or_res @@ -174,14 +175,22 @@ def GetRun( fab_id=run.fab_id, fab_version=run.fab_version, override_config=user_config_to_proto(run.override_config), + fab_hash=run.fab_hash, ) ) def GetFab( self, request: GetFabRequest, context: grpc.ServicerContext ) -> GetFabResponse: - """Will be implemented later.""" - raise NotImplementedError + """Get FAB from Ffs.""" + log(DEBUG, "DriverServicer.GetFab") + + ffs: Ffs = self.ffs_factory.ffs() + if result := ffs.get(request.hash_str): + fab = Fab(request.hash_str, result[0]) + return GetFabResponse(fab=fab_to_proto(fab)) + + raise ValueError(f"Found no FAB with hash: {request.hash_str}") def _raise_if(validation_error: bool, detail: str) -> None: diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py index a53124124c29..c14bca93a127 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py @@ -35,6 +35,7 @@ PushTaskResResponse, ) from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 +from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.fleet.message_handler import message_handler from flwr.server.superlink.state import StateFactory @@ -42,8 +43,9 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer): """Fleet API servicer.""" - def __init__(self, state_factory: StateFactory) -> None: + def __init__(self, state_factory: StateFactory, ffs_factory: FfsFactory) -> None: self.state_factory = state_factory + self.ffs_factory = ffs_factory def CreateNode( self, request: CreateNodeRequest, context: grpc.ServicerContext @@ -106,5 +108,9 @@ def GetRun( def GetFab( self, request: GetFabRequest, context: grpc.ServicerContext ) -> GetFabResponse: - """Will be implemented later.""" - raise NotImplementedError + """Get FAB.""" + log(DEBUG, "DriverServicer.GetFab") + return message_handler.get_fab( + request=request, + ffs=self.ffs_factory.ffs(), + ) diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py index b87a4293a775..ece443a816cb 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py @@ -43,6 +43,7 @@ from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 from flwr.proto.task_pb2 import Task, TaskRes # pylint: disable=E0611 from flwr.server.app import ADDRESS_FLEET_API_GRPC_RERE, _run_fleet_api_grpc_rere +from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.state.state_factory import StateFactory from .server_interceptor import ( @@ -62,6 +63,8 @@ def setUp(self) -> None: state_factory = StateFactory(":flwr-in-memory-state:") self.state = state_factory.state() + ffs_factory = FfsFactory(".") + self.ffs = ffs_factory.ffs() self.state.store_server_private_public_key( private_key_to_bytes(self._server_private_key), public_key_to_bytes(self._server_public_key), @@ -72,7 +75,11 @@ def setUp(self) -> None: self._server_interceptor = AuthenticateServerInterceptor(self.state) self._server: grpc.Server = _run_fleet_api_grpc_rere( - ADDRESS_FLEET_API_GRPC_RERE, state_factory, None, [self._server_interceptor] + ADDRESS_FLEET_API_GRPC_RERE, + state_factory, + ffs_factory, + None, + [self._server_interceptor], ) self._channel = grpc.insecure_channel("localhost:9092") diff --git a/src/py/flwr/server/superlink/fleet/message_handler/message_handler.py b/src/py/flwr/server/superlink/fleet/message_handler/message_handler.py index 30865f04d373..64f9ac609998 100644 --- a/src/py/flwr/server/superlink/fleet/message_handler/message_handler.py +++ b/src/py/flwr/server/superlink/fleet/message_handler/message_handler.py @@ -19,7 +19,9 @@ from typing import List, Optional from uuid import UUID -from flwr.common.serde import user_config_to_proto +from flwr.common.serde import fab_to_proto, user_config_to_proto +from flwr.common.typing import Fab +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -40,6 +42,7 @@ Run, ) from flwr.proto.task_pb2 import TaskIns, TaskRes # pylint: disable=E0611 +from flwr.server.superlink.ffs.ffs import Ffs from flwr.server.superlink.state import State @@ -124,5 +127,17 @@ def get_run( fab_id=run.fab_id, fab_version=run.fab_version, override_config=user_config_to_proto(run.override_config), + fab_hash=run.fab_hash, ) ) + + +def get_fab( + request: GetFabRequest, ffs: Ffs # pylint: disable=W0613 +) -> GetFabResponse: + """Get FAB.""" + if result := ffs.get(request.hash_str): + fab = Fab(request.hash_str, result[0]) + return GetFabResponse(fab=fab_to_proto(fab)) + + raise ValueError(f"Found no FAB with hash: {request.hash_str}") From 2f01ba328d324a4165ddace0aa51738ce1ab3124 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 16 Aug 2024 12:07:28 +0100 Subject: [PATCH 072/188] feat(framework) Add `ClientApp` process function (#3977) Co-authored-by: Daniel J. Beutel --- src/py/flwr/cli/run/run.py | 9 +- src/py/flwr/client/app.py | 25 +++ src/py/flwr/client/process/process.py | 143 ++++++++++++++++++ src/py/flwr/client/process/utils.py | 108 +++++++++++++ src/py/flwr/client/supernode/app.py | 94 +----------- .../server/superlink/fleet/vce/vce_api.py | 2 +- 6 files changed, 287 insertions(+), 94 deletions(-) create mode 100644 src/py/flwr/client/process/process.py create mode 100644 src/py/flwr/client/process/utils.py diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index fe7b2f32a104..2df14969e24e 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -35,6 +35,11 @@ from flwr.proto.exec_pb2_grpc import ExecStub +def on_channel_state_change(channel_connectivity: str) -> None: + """Log channel connectivity.""" + log(DEBUG, channel_connectivity) + + # pylint: disable-next=too-many-locals def run( app: Annotated[ @@ -122,10 +127,6 @@ def _run_with_superexec( config_overrides: Optional[List[str]], ) -> None: - def on_channel_state_change(channel_connectivity: str) -> None: - """Log channel connectivity.""" - log(DEBUG, channel_connectivity) - insecure_str = federation_config.get("insecure") if root_certificates := federation_config.get("root-certificates"): root_certificates_bytes = Path(root_certificates).read_bytes() diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index e20eee78e631..e42c4d462fed 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -22,6 +22,7 @@ from pathlib import Path from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union +import grpc from cryptography.hazmat.primitives.asymmetric import ec from grpc import RpcError @@ -43,6 +44,8 @@ from flwr.common.message import Error from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential from flwr.common.typing import Fab, Run, UserConfig +from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server +from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server from .grpc_adapter_client.connection import grpc_adapter from .grpc_client.connection import grpc_connection @@ -50,6 +53,9 @@ from .message_handler.message_handler import handle_control_message from .node_state import NodeState from .numpy_client import NumPyClient +from .process.clientappio_servicer import ClientAppIoServicer + +ADDRESS_CLIENTAPPIO_API_GRPC_RERE = "0.0.0.0:9094" def _check_actionable_client( @@ -667,3 +673,22 @@ def signal_handler(sig, frame): # type: ignore signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) + + +def run_clientappio_api_grpc( + address: str = ADDRESS_CLIENTAPPIO_API_GRPC_RERE, +) -> Tuple[grpc.Server, ClientAppIoServicer]: + """Run ClientAppIo API gRPC server.""" + clientappio_servicer: grpc.Server = ClientAppIoServicer() + clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server + clientappio_grpc_server = generic_create_grpc_server( + servicer_and_add_fn=( + clientappio_servicer, + clientappio_add_servicer_to_server_fn, + ), + server_address=address, + max_message_length=GRPC_MAX_MESSAGE_LENGTH, + ) + log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address) + clientappio_grpc_server.start() + return clientappio_grpc_server, clientappio_servicer diff --git a/src/py/flwr/client/process/process.py b/src/py/flwr/client/process/process.py new file mode 100644 index 000000000000..a1841940823c --- /dev/null +++ b/src/py/flwr/client/process/process.py @@ -0,0 +1,143 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower ClientApp process.""" + +from logging import DEBUG, ERROR, INFO +from typing import Tuple + +import grpc + +from flwr.client.client_app import ClientApp, LoadClientAppError +from flwr.common import Context, Message +from flwr.common.constant import ErrorCode +from flwr.common.grpc import create_channel +from flwr.common.logger import log +from flwr.common.message import Error +from flwr.common.serde import ( + context_from_proto, + context_to_proto, + message_from_proto, + message_to_proto, + run_from_proto, +) +from flwr.common.typing import Run + +# pylint: disable=E0611 +from flwr.proto.clientappio_pb2 import ( + PullClientAppInputsRequest, + PullClientAppInputsResponse, + PushClientAppOutputsRequest, + PushClientAppOutputsResponse, +) +from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub + +from .utils import _get_load_client_app_fn + + +def on_channel_state_change(channel_connectivity: str) -> None: + """Log channel connectivity.""" + log(DEBUG, channel_connectivity) + + +def _run_clientapp( # pylint: disable=R0914 + address: str, + token: int, +) -> None: + """Run Flower ClientApp process. + + Parameters + ---------- + address : str + Address of SuperNode + token : int + Unique SuperNode token for ClientApp-SuperNode authentication + """ + channel = create_channel( + server_address=address, + insecure=True, + ) + channel.subscribe(on_channel_state_change) + + try: + stub = ClientAppIoStub(channel) + + # Pull Message, Context, and Run from SuperNode + message, context, run = pull_message(stub=stub, token=token) + + load_client_app_fn = _get_load_client_app_fn( + default_app_ref="", + app_path=None, + multi_app=True, + flwr_dir=None, + ) + + try: + # Load ClientApp + client_app: ClientApp = load_client_app_fn(run.fab_id, run.fab_version) + + # Execute ClientApp + reply_message = client_app(message=message, context=context) + except Exception as ex: # pylint: disable=broad-exception-caught + # Don't update/change NodeState + + e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION + # Ex fmt: ":<'division by zero'>" + reason = str(type(ex)) + ":<'" + str(ex) + "'>" + exc_entity = "ClientApp" + if isinstance(ex, LoadClientAppError): + reason = "An exception was raised when attempting to load `ClientApp`" + e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION + + log(ERROR, "%s raised an exception", exc_entity, exc_info=ex) + + # Create error message + reply_message = message.create_error_reply( + error=Error(code=e_code, reason=reason) + ) + + # Push Message and Context to SuperNode + _ = push_message(stub=stub, token=token, message=reply_message, context=context) + + except KeyboardInterrupt: + log(INFO, "Closing connection") + except grpc.RpcError as e: + log(ERROR, "GRPC error occurred: %s", str(e)) + finally: + channel.close() + + +def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run]: + """Pull message from SuperNode to ClientApp.""" + res: PullClientAppInputsResponse = stub.PullClientAppInputs( + PullClientAppInputsRequest(token=token) + ) + message = message_from_proto(res.message) + context = context_from_proto(res.context) + run = run_from_proto(res.run) + return message, context, run + + +def push_message( + stub: grpc.Channel, token: int, message: Message, context: Context +) -> PushClientAppOutputsResponse: + """Push message to SuperNode from ClientApp.""" + proto_message = message_to_proto(message) + proto_context = context_to_proto(context) + res: PushClientAppOutputsResponse = stub.PushClientAppOutputs( + PushClientAppOutputsRequest( + token=token, message=proto_message, context=proto_context + ) + ) + return res diff --git a/src/py/flwr/client/process/utils.py b/src/py/flwr/client/process/utils.py new file mode 100644 index 000000000000..e52eba93a92b --- /dev/null +++ b/src/py/flwr/client/process/utils.py @@ -0,0 +1,108 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower ClientApp loading utils.""" + +from logging import DEBUG +from pathlib import Path +from typing import Callable, Optional + +from flwr.client.client_app import ClientApp, LoadClientAppError +from flwr.common.config import ( + get_flwr_dir, + get_metadata_from_config, + get_project_config, + get_project_dir, +) +from flwr.common.logger import log +from flwr.common.object_ref import load_app, validate + + +def _get_load_client_app_fn( + default_app_ref: str, + app_path: Optional[str], + multi_app: bool, + flwr_dir: Optional[str] = None, +) -> Callable[[str, str], ClientApp]: + """Get the load_client_app_fn function. + + If `multi_app` is True, this function loads the specified ClientApp + based on `fab_id` and `fab_version`. If `fab_id` is empty, a default + ClientApp will be loaded. + + If `multi_app` is False, it ignores `fab_id` and `fab_version` and + loads a default ClientApp. + """ + if not multi_app: + log( + DEBUG, + "Flower SuperNode will load and validate ClientApp `%s`", + default_app_ref, + ) + + valid, error_msg = validate(default_app_ref, project_dir=app_path) + if not valid and error_msg: + raise LoadClientAppError(error_msg) from None + + def _load(fab_id: str, fab_version: str) -> ClientApp: + runtime_app_dir = Path(app_path if app_path else "").absolute() + # If multi-app feature is disabled + if not multi_app: + # Set app reference + client_app_ref = default_app_ref + # If multi-app feature is enabled but app directory is provided + elif app_path is not None: + config = get_project_config(runtime_app_dir) + this_fab_version, this_fab_id = get_metadata_from_config(config) + + if this_fab_version != fab_version or this_fab_id != fab_id: + raise LoadClientAppError( + f"FAB ID or version mismatch: Expected FAB ID '{this_fab_id}' and " + f"FAB version '{this_fab_version}', but received FAB ID '{fab_id}' " + f"and FAB version '{fab_version}'.", + ) from None + + # log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.") + + # Set app reference + client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"] + # If multi-app feature is enabled + else: + try: + runtime_app_dir = get_project_dir( + fab_id, fab_version, get_flwr_dir(flwr_dir) + ) + config = get_project_config(runtime_app_dir) + except Exception as e: + raise LoadClientAppError("Failed to load ClientApp") from e + + # Set app reference + client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"] + + # Load ClientApp + log( + DEBUG, + "Loading ClientApp `%s`", + client_app_ref, + ) + client_app = load_app(client_app_ref, LoadClientAppError, runtime_app_dir) + + if not isinstance(client_app, ClientApp): + raise LoadClientAppError( + f"Attribute {client_app_ref} is not of type {ClientApp}", + ) from None + + return client_app + + return _load diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 5840b57c0ab6..b2cb1b5f033e 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -18,7 +18,7 @@ import sys from logging import DEBUG, INFO, WARN from pathlib import Path -from typing import Callable, Optional, Tuple +from typing import Optional, Tuple from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives.asymmetric import ec @@ -27,15 +27,8 @@ load_ssh_public_key, ) -from flwr.client.client_app import ClientApp, LoadClientAppError from flwr.common import EventType, event -from flwr.common.config import ( - get_flwr_dir, - get_metadata_from_config, - get_project_config, - get_project_dir, - parse_config_args, -) +from flwr.common.config import get_flwr_dir, parse_config_args from flwr.common.constant import ( TRANSPORT_TYPE_GRPC_ADAPTER, TRANSPORT_TYPE_GRPC_RERE, @@ -43,9 +36,10 @@ ) from flwr.common.exit_handlers import register_exit_handlers from flwr.common.logger import log, warn_deprecated_feature -from flwr.common.object_ref import load_app, validate from ..app import _start_client_internal +from ..process.process import _run_clientapp +from ..process.utils import _get_load_client_app_fn ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092" @@ -143,6 +137,7 @@ def flwr_clientapp() -> None: args.address, args.token, ) + _run_clientapp(address=args.address, token=int(args.token)) def _warn_deprecated_server_arg(args: argparse.Namespace) -> None: @@ -200,85 +195,6 @@ def _get_certificates(args: argparse.Namespace) -> Optional[bytes]: return root_certificates -def _get_load_client_app_fn( - default_app_ref: str, - app_path: Optional[str], - multi_app: bool, - flwr_dir: Optional[str] = None, -) -> Callable[[str, str], ClientApp]: - """Get the load_client_app_fn function. - - If `multi_app` is True, this function loads the specified ClientApp - based on `fab_id` and `fab_version`. If `fab_id` is empty, a default - ClientApp will be loaded. - - If `multi_app` is False, it ignores `fab_id` and `fab_version` and - loads a default ClientApp. - """ - if not multi_app: - log( - DEBUG, - "Flower SuperNode will load and validate ClientApp `%s`", - default_app_ref, - ) - - valid, error_msg = validate(default_app_ref, project_dir=app_path) - if not valid and error_msg: - raise LoadClientAppError(error_msg) from None - - def _load(fab_id: str, fab_version: str) -> ClientApp: - runtime_app_dir = Path(app_path if app_path else "").absolute() - # If multi-app feature is disabled - if not multi_app: - # Set app reference - client_app_ref = default_app_ref - # If multi-app feature is enabled but app directory is provided - elif app_path is not None: - config = get_project_config(runtime_app_dir) - this_fab_version, this_fab_id = get_metadata_from_config(config) - - if this_fab_version != fab_version or this_fab_id != fab_id: - raise LoadClientAppError( - f"FAB ID or version mismatch: Expected FAB ID '{this_fab_id}' and " - f"FAB version '{this_fab_version}', but received FAB ID '{fab_id}' " - f"and FAB version '{fab_version}'.", - ) from None - - # log(WARN, "FAB ID is not provided; the default ClientApp will be loaded.") - - # Set app reference - client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"] - # If multi-app feature is enabled - else: - try: - runtime_app_dir = get_project_dir( - fab_id, fab_version, get_flwr_dir(flwr_dir) - ) - config = get_project_config(runtime_app_dir) - except Exception as e: - raise LoadClientAppError("Failed to load ClientApp") from e - - # Set app reference - client_app_ref = config["tool"]["flwr"]["app"]["components"]["clientapp"] - - # Load ClientApp - log( - DEBUG, - "Loading ClientApp `%s`", - client_app_ref, - ) - client_app = load_app(client_app_ref, LoadClientAppError, runtime_app_dir) - - if not isinstance(client_app, ClientApp): - raise LoadClientAppError( - f"Attribute {client_app_ref} is not of type {ClientApp}", - ) from None - - return client_app - - return _load - - def _parse_args_run_supernode() -> argparse.ArgumentParser: """Parse flower-supernode command line arguments.""" parser = argparse.ArgumentParser( diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index a8d02802a8b1..1df821d0f495 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -28,7 +28,7 @@ from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError from flwr.client.node_state import NodeState -from flwr.client.supernode.app import _get_load_client_app_fn +from flwr.client.process.utils import _get_load_client_app_fn from flwr.common.constant import ( NUM_PARTITIONS_KEY, PARTITION_ID_KEY, From 1ed82ffa190610e12aedc96c662182cde53332d0 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Fri, 16 Aug 2024 13:23:52 +0200 Subject: [PATCH 073/188] ci(*:skip) Use `flower-supernode` in reconnection e2e tests (#3982) Co-authored-by: Heng Pan --- e2e/test_reconnection.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/test_reconnection.sh b/e2e/test_reconnection.sh index 3ca1969fec9b..49f25507f047 100755 --- a/e2e/test_reconnection.sh +++ b/e2e/test_reconnection.sh @@ -29,12 +29,12 @@ sl_pid=$! echo "Starting SuperLink" sleep 3 -timeout 2m flower-client-app client:app --insecure $rest_arg --server $server_address & +timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting first client" sleep 3 -timeout 2m flower-client-app client:app --insecure $rest_arg --server $server_address & +timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & cl2_pid=$! echo "Starting second client" sleep 3 @@ -56,13 +56,13 @@ echo "Killing first client" sleep 3 # Starting new client, this is so we have enough clients to start the server-app -timeout 2m flower-client-app client:app --insecure $rest_arg --server $server_address & +timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting new client" sleep 5 # We start the server-app to begining the training -timeout 2m flower-server-app ./.. $rest_arg --server $server_app_address & +timeout 2m flower-server-app ./.. $rest_arg --superlink $server_app_address & pid=$! echo "Starting server-app to start training" @@ -74,7 +74,7 @@ echo "Killing first client" sleep 1 # Restart first client so enough clients are connected to continue the FL rounds -timeout 2m flower-client-app client:app --insecure $rest_arg --server $server_address & +timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting new client" From 61e8282e3a488a8ffbf0dab5fd5dc71db301ae30 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 16 Aug 2024 12:32:07 +0100 Subject: [PATCH 074/188] fix(framework) Remove `_` prefix from cross-module function names (#4026) --- src/py/flwr/client/app.py | 4 ++-- src/py/flwr/client/process/process.py | 6 +++--- src/py/flwr/client/process/utils.py | 2 +- src/py/flwr/client/supernode/app.py | 16 ++++++++-------- .../flwr/server/superlink/fleet/vce/vce_api.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index e42c4d462fed..ef56c45939b4 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -164,7 +164,7 @@ class `flwr.client.Client` (default: None) >>> ) """ event(EventType.START_CLIENT_ENTER) - _start_client_internal( + start_client_internal( server_address=server_address, node_config={}, load_client_app_fn=None, @@ -185,7 +185,7 @@ class `flwr.client.Client` (default: None) # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements -def _start_client_internal( +def start_client_internal( *, server_address: str, node_config: UserConfig, diff --git a/src/py/flwr/client/process/process.py b/src/py/flwr/client/process/process.py index a1841940823c..91a9670e8658 100644 --- a/src/py/flwr/client/process/process.py +++ b/src/py/flwr/client/process/process.py @@ -43,7 +43,7 @@ ) from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub -from .utils import _get_load_client_app_fn +from .utils import get_load_client_app_fn def on_channel_state_change(channel_connectivity: str) -> None: @@ -51,7 +51,7 @@ def on_channel_state_change(channel_connectivity: str) -> None: log(DEBUG, channel_connectivity) -def _run_clientapp( # pylint: disable=R0914 +def run_clientapp( # pylint: disable=R0914 address: str, token: int, ) -> None: @@ -76,7 +76,7 @@ def _run_clientapp( # pylint: disable=R0914 # Pull Message, Context, and Run from SuperNode message, context, run = pull_message(stub=stub, token=token) - load_client_app_fn = _get_load_client_app_fn( + load_client_app_fn = get_load_client_app_fn( default_app_ref="", app_path=None, multi_app=True, diff --git a/src/py/flwr/client/process/utils.py b/src/py/flwr/client/process/utils.py index e52eba93a92b..d2386dd707c3 100644 --- a/src/py/flwr/client/process/utils.py +++ b/src/py/flwr/client/process/utils.py @@ -29,7 +29,7 @@ from flwr.common.object_ref import load_app, validate -def _get_load_client_app_fn( +def get_load_client_app_fn( default_app_ref: str, app_path: Optional[str], multi_app: bool, diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index b2cb1b5f033e..d0928f8201fa 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -37,9 +37,9 @@ from flwr.common.exit_handlers import register_exit_handlers from flwr.common.logger import log, warn_deprecated_feature -from ..app import _start_client_internal -from ..process.process import _run_clientapp -from ..process.utils import _get_load_client_app_fn +from ..app import start_client_internal +from ..process.process import run_clientapp +from ..process.utils import get_load_client_app_fn ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092" @@ -55,7 +55,7 @@ def run_supernode() -> None: _warn_deprecated_server_arg(args) root_certificates = _get_certificates(args) - load_fn = _get_load_client_app_fn( + load_fn = get_load_client_app_fn( default_app_ref="", app_path=args.app, flwr_dir=args.flwr_dir, @@ -63,7 +63,7 @@ def run_supernode() -> None: ) authentication_keys = _try_setup_client_authentication(args) - _start_client_internal( + start_client_internal( server_address=args.superlink, load_client_app_fn=load_fn, transport=args.transport, @@ -93,14 +93,14 @@ def run_client_app() -> None: _warn_deprecated_server_arg(args) root_certificates = _get_certificates(args) - load_fn = _get_load_client_app_fn( + load_fn = get_load_client_app_fn( default_app_ref=getattr(args, "client-app"), app_path=args.dir, multi_app=False, ) authentication_keys = _try_setup_client_authentication(args) - _start_client_internal( + start_client_internal( server_address=args.superlink, node_config=parse_config_args([args.node_config]), load_client_app_fn=load_fn, @@ -137,7 +137,7 @@ def flwr_clientapp() -> None: args.address, args.token, ) - _run_clientapp(address=args.address, token=int(args.token)) + run_clientapp(address=args.address, token=int(args.token)) def _warn_deprecated_server_arg(args: argparse.Namespace) -> None: diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index 1df821d0f495..aa726f902d47 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -28,7 +28,7 @@ from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError from flwr.client.node_state import NodeState -from flwr.client.process.utils import _get_load_client_app_fn +from flwr.client.process.utils import get_load_client_app_fn from flwr.common.constant import ( NUM_PARTITIONS_KEY, PARTITION_ID_KEY, @@ -345,7 +345,7 @@ def backend_fn() -> Backend: def _load() -> ClientApp: if client_app_attr: - app = _get_load_client_app_fn( + app = get_load_client_app_fn( default_app_ref=client_app_attr, app_path=app_dir, flwr_dir=flwr_dir, From 3203446041ae749071c07ee77a66887883a7823f Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 16 Aug 2024 12:43:33 +0100 Subject: [PATCH 075/188] feat(framework) Add `ClientAppIo` servicer test (#4001) --- .../process/clientappio_servicer_test.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/py/flwr/client/process/clientappio_servicer_test.py b/src/py/flwr/client/process/clientappio_servicer_test.py index b06e9eb0e1c0..4831825acf30 100644 --- a/src/py/flwr/client/process/clientappio_servicer_test.py +++ b/src/py/flwr/client/process/clientappio_servicer_test.py @@ -15,10 +15,25 @@ """Test the ClientAppIo API servicer.""" import unittest +from unittest.mock import Mock, patch +from flwr.client.process.process import pull_message, push_message from flwr.common import Context, Message, typing +from flwr.common.serde import ( + clientappstatus_from_proto, + clientappstatus_to_proto, + message_to_proto, +) from flwr.common.serde_test import RecordMaker +# pylint:disable=E0611 +from flwr.proto.clientappio_pb2 import ( + PullClientAppInputsResponse, + PushClientAppOutputsResponse, +) +from flwr.proto.message_pb2 import Context as ProtoContext +from flwr.proto.run_pb2 import Run as ProtoRun + from .clientappio_servicer import ( ClientAppIoInputs, ClientAppIoOutputs, @@ -33,9 +48,15 @@ def setUp(self) -> None: """Initialize.""" self.servicer = ClientAppIoServicer() self.maker = RecordMaker() + self.mock_stub = Mock() + self.patcher = patch( + "flwr.client.process.process.ClientAppIoStub", return_value=self.mock_stub + ) + self.patcher.start() def tearDown(self) -> None: """Cleanup.""" + self.patcher.stop() def test_set_inputs(self) -> None: """Test setting ClientApp inputs.""" @@ -116,3 +137,60 @@ def test_get_outputs(self) -> None: assert output == client_output assert self.servicer.clientapp_input is None assert self.servicer.clientapp_output is None + + def test_pull_clientapp_inputs(self) -> None: + """Test pulling messages from SuperNode.""" + # Prepare + mock_message = Message( + metadata=self.maker.metadata(), + content=self.maker.recordset(3, 2, 1), + ) + mock_response = PullClientAppInputsResponse( + message=message_to_proto(mock_message), + context=ProtoContext(node_id=123), + run=ProtoRun(run_id=61016, fab_id="mock/mock", fab_version="v1.0.0"), + ) + self.mock_stub.PullClientAppInputs.return_value = mock_response + + # Execute + message, context, run = pull_message(self.mock_stub, token=456) + + # Assert + self.mock_stub.PullClientAppInputs.assert_called_once() + self.assertEqual(len(message.content.parameters_records), 3) + self.assertEqual(len(message.content.metrics_records), 2) + self.assertEqual(len(message.content.configs_records), 1) + self.assertEqual(context.node_id, 123) + self.assertEqual(run.run_id, 61016) + self.assertEqual(run.fab_id, "mock/mock") + self.assertEqual(run.fab_version, "v1.0.0") + + def test_push_clientapp_outputs(self) -> None: + """Test pushing messages to SuperNode.""" + # Prepare + message = Message( + metadata=self.maker.metadata(), + content=self.maker.recordset(2, 2, 1), + ) + context = Context( + node_id=1, + node_config={"nodeconfig1": 4.2}, + state=self.maker.recordset(2, 2, 1), + run_config={"runconfig1": 6.1}, + ) + code = typing.ClientAppOutputCode.SUCCESS + status_proto = clientappstatus_to_proto( + status=typing.ClientAppOutputStatus(code=code, message="SUCCESS"), + ) + mock_response = PushClientAppOutputsResponse(status=status_proto) + self.mock_stub.PushClientAppOutputs.return_value = mock_response + + # Execute + res = push_message( + stub=self.mock_stub, token=789, message=message, context=context + ) + status = clientappstatus_from_proto(res.status) + + # Assert + self.mock_stub.PushClientAppOutputs.assert_called_once() + self.assertEqual(status.message, "SUCCESS") From 425af4f8b75797696f411da616ecfbf5aea83067 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 16 Aug 2024 15:34:15 +0100 Subject: [PATCH 076/188] fix(framework) Improve logging for centralized evaluation (#3986) --- src/py/flwr/server/server.py | 4 +++- src/py/flwr/server/workflow/default_workflows.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/server.py b/src/py/flwr/server/server.py index f1bfb6f0533b..5e2a0c6b2719 100644 --- a/src/py/flwr/server/server.py +++ b/src/py/flwr/server/server.py @@ -91,7 +91,7 @@ def fit(self, num_rounds: int, timeout: Optional[float]) -> Tuple[History, float # Initialize parameters log(INFO, "[INIT]") self.parameters = self._get_initial_parameters(server_round=0, timeout=timeout) - log(INFO, "Evaluating initial global parameters") + log(INFO, "Starting evaluation of initial global parameters") res = self.strategy.evaluate(0, parameters=self.parameters) if res is not None: log( @@ -102,6 +102,8 @@ def fit(self, num_rounds: int, timeout: Optional[float]) -> Tuple[History, float ) history.add_loss_centralized(server_round=0, loss=res[0]) history.add_metrics_centralized(server_round=0, metrics=res[1]) + else: + log(INFO, "Evaluation returned no results (`None`)") # Run federated learning for num_rounds start_time = timeit.default_timer() diff --git a/src/py/flwr/server/workflow/default_workflows.py b/src/py/flwr/server/workflow/default_workflows.py index 80759316da84..82d8d5d4ccb6 100644 --- a/src/py/flwr/server/workflow/default_workflows.py +++ b/src/py/flwr/server/workflow/default_workflows.py @@ -167,7 +167,7 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None: context.state.parameters_records[MAIN_PARAMS_RECORD] = paramsrecord # Evaluate initial parameters - log(INFO, "Evaluating initial global parameters") + log(INFO, "Starting evaluation of initial global parameters") parameters = compat.parametersrecord_to_parameters(paramsrecord, keep_input=True) res = context.strategy.evaluate(0, parameters=parameters) if res is not None: @@ -179,6 +179,8 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None: ) context.history.add_loss_centralized(server_round=0, loss=res[0]) context.history.add_metrics_centralized(server_round=0, metrics=res[1]) + else: + log(INFO, "Evaluation returned no results (`None`)") def default_centralized_evaluation_workflow(_: Driver, context: Context) -> None: From e366b3a8aa060b855e401f5902639e32dbe517a5 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 16 Aug 2024 18:04:53 +0200 Subject: [PATCH 077/188] feat(framework) Add end-to-end FAB delivery (#3852) Co-authored-by: Taner Topal Co-authored-by: Daniel J. Beutel --- src/py/flwr/client/app.py | 23 +++++++++++++++++------ src/py/flwr/client/supernode/app.py | 3 +-- src/py/flwr/server/run_serverapp.py | 20 ++++++++++++++++++-- src/py/flwr/superexec/deployment.py | 17 ++++++++--------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index ef56c45939b4..bc42075aa9e5 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -26,6 +26,8 @@ from cryptography.hazmat.primitives.asymmetric import ec from grpc import RpcError +from flwr.cli.config_utils import get_fab_metadata +from flwr.cli.install import install_from_fab from flwr.client.client import Client from flwr.client.client_app import ClientApp, LoadClientAppError from flwr.client.typing import ClientFnExt @@ -339,7 +341,7 @@ def _on_backoff(retry_state: RetryState) -> None: root_certificates, authentication_keys, ) as conn: - receive, send, create_node, delete_node, get_run, _ = conn + receive, send, create_node, delete_node, get_run, get_fab = conn # Register node when connecting the first time if node_state is None: @@ -406,9 +408,16 @@ def _on_backoff(retry_state: RetryState) -> None: else: runs[run_id] = Run(run_id, "", "", "", {}) + run: Run = runs[run_id] + if get_fab is not None and run.fab_hash: + fab = get_fab(run.fab_hash) + install_from_fab(fab.content, flwr_path, True) + else: + fab = None + # Register context for this run node_state.register_context( - run_id=run_id, run=runs[run_id], flwr_path=flwr_path + run_id=run_id, run=run, flwr_path=flwr_path ) # Retrieve context for this run @@ -423,10 +432,12 @@ def _on_backoff(retry_state: RetryState) -> None: # Handle app loading and task message try: # Load ClientApp instance - run: Run = runs[run_id] - client_app: ClientApp = load_client_app_fn( - run.fab_id, run.fab_version - ) + if fab: + fab_id, fab_version = get_fab_metadata(fab.content) + else: + fab_id, fab_version = run.fab_id, run.fab_version + + client_app: ClientApp = load_client_app_fn(fab_id, fab_version) # Execute ClientApp reply_message = client_app(message=message, context=context) diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index d0928f8201fa..4370d7d1219d 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -28,7 +28,7 @@ ) from flwr.common import EventType, event -from flwr.common.config import get_flwr_dir, parse_config_args +from flwr.common.config import parse_config_args from flwr.common.constant import ( TRANSPORT_TYPE_GRPC_ADAPTER, TRANSPORT_TYPE_GRPC_RERE, @@ -73,7 +73,6 @@ def run_supernode() -> None: max_retries=args.max_retries, max_wait_time=args.max_wait_time, node_config=parse_config_args([args.node_config]), - flwr_path=get_flwr_dir(args.flwr_dir), ) # Graceful shutdown diff --git a/src/py/flwr/server/run_serverapp.py b/src/py/flwr/server/run_serverapp.py index 76eae30330d9..8f67c917c8ed 100644 --- a/src/py/flwr/server/run_serverapp.py +++ b/src/py/flwr/server/run_serverapp.py @@ -21,6 +21,8 @@ from pathlib import Path from typing import Optional +from flwr.cli.config_utils import get_fab_metadata +from flwr.cli.install import install_from_fab from flwr.common import Context, EventType, RecordSet, event from flwr.common.config import ( get_flwr_dir, @@ -36,6 +38,7 @@ CreateRunRequest, CreateRunResponse, ) +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from .driver import Driver from .driver.grpc_driver import GrpcDriver @@ -87,7 +90,8 @@ def _load() -> ServerApp: log(DEBUG, "ServerApp finished running.") -def run_server_app() -> None: # pylint: disable=too-many-branches +# pylint: disable-next=too-many-branches,too-many-statements,too-many-locals +def run_server_app() -> None: """Run Flower server app.""" event(EventType.RUN_SERVER_APP_ENTER) @@ -164,7 +168,19 @@ def run_server_app() -> None: # pylint: disable=too-many-branches ) flwr_dir = get_flwr_dir(args.flwr_dir) run_ = driver.run - app_path = str(get_project_dir(run_.fab_id, run_.fab_version, flwr_dir)) + if run_.fab_hash: + fab_req = GetFabRequest(hash_str=run_.fab_hash) + # pylint: disable-next=W0212 + fab_res: GetFabResponse = driver._stub.GetFab(fab_req) + if fab_res.fab.hash_str != run_.fab_hash: + raise ValueError("FAB hashes don't match.") + + install_from_fab(fab_res.fab.content, flwr_dir, True) + fab_id, fab_version = get_fab_metadata(fab_res.fab.content) + else: + fab_id, fab_version = run_.fab_id, run_.fab_version + + app_path = str(get_project_dir(fab_id, fab_version, flwr_dir)) config = get_project_config(app_path) else: # User provided `app_dir`, but not `--run-id` diff --git a/src/py/flwr/superexec/deployment.py b/src/py/flwr/superexec/deployment.py index fd09b512a52c..2354e047a1ec 100644 --- a/src/py/flwr/superexec/deployment.py +++ b/src/py/flwr/superexec/deployment.py @@ -14,6 +14,7 @@ # ============================================================================== """Deployment engine executor.""" +import hashlib import subprocess from logging import ERROR, INFO from pathlib import Path @@ -21,12 +22,11 @@ from typing_extensions import override -from flwr.cli.config_utils import get_fab_metadata from flwr.cli.install import install_from_fab from flwr.common.grpc import create_channel from flwr.common.logger import log -from flwr.common.serde import user_config_to_proto -from flwr.common.typing import UserConfig +from flwr.common.serde import fab_to_proto, user_config_to_proto +from flwr.common.typing import Fab, UserConfig from flwr.proto.driver_pb2 import CreateRunRequest # pylint: disable=E0611 from flwr.proto.driver_pb2_grpc import DriverStub from flwr.server.driver.grpc_driver import DEFAULT_SERVER_ADDRESS_DRIVER @@ -113,8 +113,7 @@ def _connect(self) -> None: def _create_run( self, - fab_id: str, - fab_version: str, + fab: Fab, override_config: UserConfig, ) -> int: if self.stub is None: @@ -123,8 +122,7 @@ def _create_run( assert self.stub is not None req = CreateRunRequest( - fab_id=fab_id, - fab_version=fab_version, + fab=fab_to_proto(fab), override_config=user_config_to_proto(override_config), ) res = self.stub.CreateRun(request=req) @@ -140,11 +138,12 @@ def start_run( """Start run using the Flower Deployment Engine.""" try: # Install FAB to flwr dir - fab_version, fab_id = get_fab_metadata(fab_file) install_from_fab(fab_file, None, True) # Call SuperLink to create run - run_id: int = self._create_run(fab_id, fab_version, override_config) + run_id: int = self._create_run( + Fab(hashlib.sha256(fab_file).hexdigest(), fab_file), override_config + ) log(INFO, "Created run %s", str(run_id)) command = [ From ac1cbff48656c88b0c69e4b478d9d61342769d95 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Fri, 16 Aug 2024 18:31:06 +0200 Subject: [PATCH 078/188] ci(*:skip) Update e2e tests directory structure (#3981) --- .github/workflows/e2e.yml | 54 ++++++++----- .../README.md | 0 .../certificate.conf | 0 e2e/e2e-bare-auth/e2e_bare_auth/__init__.py | 1 + .../e2e_bare_auth/client_app.py} | 0 .../e2e_bare_auth/server_app.py} | 0 .../generate.sh | 0 .../pyproject.toml | 8 +- e2e/{bare-https => e2e-bare-https}/README.md | 0 .../certificate.conf | 0 e2e/e2e-bare-https/e2e_bare_https/__init__.py | 1 + .../e2e_bare_https/client_app.py} | 2 +- .../e2e_bare_https/server_app.py | 43 ++++++++++ .../generate.sh | 0 .../pyproject.toml | 6 +- .../simulation.py | 0 e2e/{bare => e2e-bare}/README.md | 0 e2e/e2e-bare/e2e_bare/__init__.py | 1 + .../e2e_bare/client_app.py} | 0 .../e2e_bare/server_app.py} | 0 e2e/{bare => e2e-bare}/pyproject.toml | 6 +- e2e/{bare => e2e-bare}/simulation.py | 2 +- .../README.md | 0 e2e/e2e-fastai/e2e_fastai/__init__.py | 1 + .../e2e_fastai/client_app.py} | 0 e2e/e2e-fastai/e2e_fastai/server_app.py | 79 +++++++++++++++++++ .../pyproject.toml | 6 +- .../simulation.py | 2 +- e2e/{framework-jax => e2e-jax}/README.md | 0 e2e/e2e-jax/e2e_jax/__init__.py | 1 + .../e2e_jax/client_app.py} | 2 +- .../e2e_jax}/jax_training.py | 0 e2e/e2e-jax/e2e_jax/server_app.py | 79 +++++++++++++++++++ e2e/{framework-jax => e2e-jax}/pyproject.toml | 6 +- .../simulation.py | 2 +- .../.gitignore | 0 .../README.md | 0 e2e/e2e-opacus/e2e_opacus/__init__.py | 1 + .../e2e_opacus/client_app.py} | 2 +- e2e/e2e-opacus/e2e_opacus/server_app.py | 79 +++++++++++++++++++ .../pyproject.toml | 6 +- .../simulation.py | 2 +- .../README.md | 0 e2e/e2e-pandas/e2e_pandas/__init__.py | 1 + .../e2e_pandas/client_app.py} | 5 +- .../e2e_pandas/server_app.py} | 2 +- .../e2e_pandas}/strategy.py | 0 .../pyproject.toml | 6 +- .../simulation.py | 4 +- .../README.md | 0 .../e2e_pytorch_lightning/__init__.py | 1 + .../e2e_pytorch_lightning/client_app.py} | 2 +- .../e2e_pytorch_lightning}/mnist.py | 4 +- .../e2e_pytorch_lightning/server_app.py | 79 +++++++++++++++++++ .../pyproject.toml | 6 +- e2e/e2e-pytorch-lightning/simulation.py | 14 ++++ .../README.md | 0 e2e/e2e-pytorch/e2e_pytorch/__init__.py | 1 + .../e2e_pytorch/client_app.py} | 4 +- e2e/e2e-pytorch/e2e_pytorch/server_app.py | 79 +++++++++++++++++++ .../pyproject.toml | 6 +- .../simulation.py | 2 +- .../simulation_next.py | 2 +- .../README.md | 2 +- .../e2e_scikit_learn/__init__.py | 1 + .../e2e_scikit_learn/client_app.py} | 2 +- .../e2e_scikit_learn/server_app.py | 79 +++++++++++++++++++ .../e2e_scikit_learn}/utils.py | 0 .../pyproject.toml | 6 +- e2e/e2e-scikit-learn/simulation.py | 14 ++++ e2e/e2e-tensorflow/README.md | 5 ++ e2e/e2e-tensorflow/e2e_tensorflow/__init__.py | 1 + .../e2e_tensorflow/client_app.py} | 0 .../e2e_tensorflow/server_app.py | 79 +++++++++++++++++++ .../pyproject.toml | 6 +- .../simulation.py | 2 +- .../simulation_next.py | 2 +- e2e/framework-scikit-learn/simulation.py | 14 ---- e2e/framework-tensorflow/README.md | 5 -- e2e/framework-tensorflow/simulation.py | 14 ---- e2e/pyproject.toml | 2 +- e2e/test_legacy.sh | 21 ++--- e2e/test_reconnection.sh | 10 +-- e2e/test_superlink.sh | 9 +-- 84 files changed, 747 insertions(+), 137 deletions(-) rename e2e/{bare-client-auth => e2e-bare-auth}/README.md (100%) rename e2e/{bare-client-auth => e2e-bare-auth}/certificate.conf (100%) create mode 100644 e2e/e2e-bare-auth/e2e_bare_auth/__init__.py rename e2e/{bare-client-auth/client.py => e2e-bare-auth/e2e_bare_auth/client_app.py} (100%) rename e2e/{bare-https/server.py => e2e-bare-auth/e2e_bare_auth/server_app.py} (100%) rename e2e/{bare-client-auth => e2e-bare-auth}/generate.sh (100%) rename e2e/{bare-client-auth => e2e-bare-auth}/pyproject.toml (73%) rename e2e/{bare-https => e2e-bare-https}/README.md (100%) rename e2e/{bare-https => e2e-bare-https}/certificate.conf (100%) create mode 100644 e2e/e2e-bare-https/e2e_bare_https/__init__.py rename e2e/{bare-https/client.py => e2e-bare-https/e2e_bare_https/client_app.py} (93%) create mode 100644 e2e/e2e-bare-https/e2e_bare_https/server_app.py rename e2e/{bare-https => e2e-bare-https}/generate.sh (100%) rename e2e/{bare-https => e2e-bare-https}/pyproject.toml (83%) rename e2e/{bare-https => e2e-bare-https}/simulation.py (100%) rename e2e/{bare => e2e-bare}/README.md (100%) create mode 100644 e2e/e2e-bare/e2e_bare/__init__.py rename e2e/{bare/client.py => e2e-bare/e2e_bare/client_app.py} (100%) rename e2e/{server.py => e2e-bare/e2e_bare/server_app.py} (100%) rename e2e/{bare => e2e-bare}/pyproject.toml (86%) rename e2e/{bare => e2e-bare}/simulation.py (97%) rename e2e/{framework-fastai => e2e-fastai}/README.md (100%) create mode 100644 e2e/e2e-fastai/e2e_fastai/__init__.py rename e2e/{framework-fastai/client.py => e2e-fastai/e2e_fastai/client_app.py} (100%) create mode 100644 e2e/e2e-fastai/e2e_fastai/server_app.py rename e2e/{framework-fastai => e2e-fastai}/pyproject.toml (86%) rename e2e/{framework-fastai => e2e-fastai}/simulation.py (86%) rename e2e/{framework-jax => e2e-jax}/README.md (100%) create mode 100644 e2e/e2e-jax/e2e_jax/__init__.py rename e2e/{framework-jax/client.py => e2e-jax/e2e_jax/client_app.py} (98%) rename e2e/{framework-jax => e2e-jax/e2e_jax}/jax_training.py (100%) create mode 100644 e2e/e2e-jax/e2e_jax/server_app.py rename e2e/{framework-jax => e2e-jax}/pyproject.toml (88%) rename e2e/{framework-opacus => e2e-jax}/simulation.py (87%) rename e2e/{framework-opacus => e2e-opacus}/.gitignore (100%) rename e2e/{framework-opacus => e2e-opacus}/README.md (100%) create mode 100644 e2e/e2e-opacus/e2e_opacus/__init__.py rename e2e/{framework-opacus/client.py => e2e-opacus/e2e_opacus/client_app.py} (98%) create mode 100644 e2e/e2e-opacus/e2e_opacus/server_app.py rename e2e/{framework-opacus => e2e-opacus}/pyproject.toml (86%) rename e2e/{framework-jax => e2e-opacus}/simulation.py (86%) rename e2e/{framework-pandas => e2e-pandas}/README.md (100%) create mode 100644 e2e/e2e-pandas/e2e_pandas/__init__.py rename e2e/{framework-pandas/client.py => e2e-pandas/e2e_pandas/client_app.py} (87%) rename e2e/{framework-pandas/server.py => e2e-pandas/e2e_pandas/server_app.py} (96%) rename e2e/{framework-pandas => e2e-pandas/e2e_pandas}/strategy.py (100%) rename e2e/{framework-pandas => e2e-pandas}/pyproject.toml (88%) rename e2e/{framework-pandas => e2e-pandas}/simulation.py (84%) rename e2e/{framework-pytorch-lightning => e2e-pytorch-lightning}/README.md (100%) create mode 100644 e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/__init__.py rename e2e/{framework-pytorch-lightning/client.py => e2e-pytorch-lightning/e2e_pytorch_lightning/client_app.py} (98%) rename e2e/{framework-pytorch-lightning => e2e-pytorch-lightning/e2e_pytorch_lightning}/mnist.py (94%) create mode 100644 e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/server_app.py rename e2e/{framework-pytorch-lightning => e2e-pytorch-lightning}/pyproject.toml (83%) create mode 100644 e2e/e2e-pytorch-lightning/simulation.py rename e2e/{framework-pytorch => e2e-pytorch}/README.md (100%) create mode 100644 e2e/e2e-pytorch/e2e_pytorch/__init__.py rename e2e/{framework-pytorch/client.py => e2e-pytorch/e2e_pytorch/client_app.py} (96%) create mode 100644 e2e/e2e-pytorch/e2e_pytorch/server_app.py rename e2e/{framework-pytorch => e2e-pytorch}/pyproject.toml (86%) rename e2e/{framework-pytorch => e2e-pytorch}/simulation.py (96%) rename e2e/{framework-pytorch => e2e-pytorch}/simulation_next.py (82%) rename e2e/{framework-scikit-learn => e2e-scikit-learn}/README.md (71%) create mode 100644 e2e/e2e-scikit-learn/e2e_scikit_learn/__init__.py rename e2e/{framework-scikit-learn/client.py => e2e-scikit-learn/e2e_scikit_learn/client_app.py} (98%) create mode 100644 e2e/e2e-scikit-learn/e2e_scikit_learn/server_app.py rename e2e/{framework-scikit-learn => e2e-scikit-learn/e2e_scikit_learn}/utils.py (100%) rename e2e/{framework-scikit-learn => e2e-scikit-learn}/pyproject.toml (87%) create mode 100644 e2e/e2e-scikit-learn/simulation.py create mode 100644 e2e/e2e-tensorflow/README.md create mode 100644 e2e/e2e-tensorflow/e2e_tensorflow/__init__.py rename e2e/{framework-tensorflow/client.py => e2e-tensorflow/e2e_tensorflow/client_app.py} (100%) create mode 100644 e2e/e2e-tensorflow/e2e_tensorflow/server_app.py rename e2e/{framework-tensorflow => e2e-tensorflow}/pyproject.toml (85%) rename e2e/{framework-pytorch-lightning => e2e-tensorflow}/simulation.py (85%) rename e2e/{framework-tensorflow => e2e-tensorflow}/simulation_next.py (81%) delete mode 100644 e2e/framework-scikit-learn/simulation.py delete mode 100644 e2e/framework-tensorflow/README.md delete mode 100644 e2e/framework-tensorflow/simulation.py diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4ad54fae8fa9..49e5b7bf1b36 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -60,45 +60,56 @@ jobs: strategy: matrix: include: - - directory: bare + - directory: e2e-bare + e2e: e2e_bare - - directory: bare-https + - directory: e2e-bare-https + e2e: e2e_bare_https - - directory: bare-client-auth + - directory: e2e-bare-auth + e2e: e2e_bare_auth - - directory: framework-jax + - directory: e2e-jax + e2e: e2e_jax - - directory: framework-pytorch + - directory: e2e-pytorch + e2e: e2e_pytorch dataset: | from torchvision.datasets import CIFAR10 CIFAR10('./data', download=True) - - directory: framework-tensorflow + - directory: e2e-tensorflow + e2e: e2e_tensorflow dataset: | import tensorflow as tf tf.keras.datasets.cifar10.load_data() - - directory: framework-opacus + - directory: e2e-opacus + e2e: e2e_opacus dataset: | from torchvision.datasets import CIFAR10 CIFAR10('./data', download=True) - - directory: framework-pytorch-lightning + - directory: e2e-pytorch-lightning + e2e: e2e_pytorch_lightning dataset: | from torchvision.datasets import MNIST MNIST('./data', download=True) - - directory: framework-scikit-learn + - directory: e2e-scikit-learn + e2e: e2e_scikit_learn dataset: | import openml openml.datasets.get_dataset(554) - - directory: framework-fastai + - directory: e2e-fastai + e2e: e2e_fastai dataset: | from fastai.vision.all import untar_data, URLs untar_data(URLs.MNIST) - - directory: framework-pandas + - directory: e2e-pandas + e2e: e2e_pandas dataset: | from pathlib import Path from sklearn.datasets import load_iris @@ -135,32 +146,35 @@ jobs: if: ${{ github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} run: | python -m pip install https://${{ env.ARTIFACT_BUCKET }}/py/${{ needs.wheel.outputs.dir }}/${{ needs.wheel.outputs.short_sha }}/${{ needs.wheel.outputs.whl_path }} + - name: Install e2e components + run: pip install . - name: Download dataset if: ${{ matrix.dataset }} run: python -c "${{ matrix.dataset }}" - name: Run edge client test - if: ${{ matrix.directory != 'bare-client-auth' }} - run: ./../test_legacy.sh "${{ matrix.directory }}" + if: ${{ matrix.directory != 'e2e-bare-auth' }} + working-directory: e2e/${{ matrix.directory }}/${{ matrix.e2e }} + run: ./../../test_legacy.sh "${{ matrix.directory }}" - name: Run virtual client test - if: ${{ matrix.directory != 'bare-client-auth' }} + if: ${{ matrix.directory != 'e2e-bare-auth' }} run: python simulation.py - name: Run simulation engine test - if: ${{ matrix.directory == 'pytorch' || matrix.directory == 'tensorflow'}} + if: ${{ matrix.directory == 'e2e-pytorch' || matrix.directory == 'e2e-tensorflow'}} run: python simulation_next.py - name: Run driver test - if: ${{ matrix.directory != 'bare-client-auth' }} + if: ${{ matrix.directory != 'e2e-bare-auth' }} run: ./../test_superlink.sh "${{ matrix.directory }}" - name: Run driver test with REST - if: ${{ matrix.directory == 'bare' }} + if: ${{ matrix.directory == 'e2e-bare' }} run: ./../test_superlink.sh bare rest - name: Run driver test with SQLite database - if: ${{ matrix.directory == 'bare' }} + if: ${{ matrix.directory == 'e2e-bare' }} run: ./../test_superlink.sh bare sqlite - name: Run driver test with client authentication - if: ${{ matrix.directory == 'bare-client-auth' }} + if: ${{ matrix.directory == 'e2e-bare-auth' }} run: ./../test_superlink.sh bare client-auth - name: Run reconnection test with SQLite database - if: ${{ matrix.directory == 'bare' }} + if: ${{ matrix.directory == 'e2e-bare' }} run: ./../test_reconnection.sh sqlite - name: Cache save Python location id: cache-save-python diff --git a/e2e/bare-client-auth/README.md b/e2e/e2e-bare-auth/README.md similarity index 100% rename from e2e/bare-client-auth/README.md rename to e2e/e2e-bare-auth/README.md diff --git a/e2e/bare-client-auth/certificate.conf b/e2e/e2e-bare-auth/certificate.conf similarity index 100% rename from e2e/bare-client-auth/certificate.conf rename to e2e/e2e-bare-auth/certificate.conf diff --git a/e2e/e2e-bare-auth/e2e_bare_auth/__init__.py b/e2e/e2e-bare-auth/e2e_bare_auth/__init__.py new file mode 100644 index 000000000000..713eba0cf451 --- /dev/null +++ b/e2e/e2e-bare-auth/e2e_bare_auth/__init__.py @@ -0,0 +1 @@ +"""bare_auth_e2e.""" diff --git a/e2e/bare-client-auth/client.py b/e2e/e2e-bare-auth/e2e_bare_auth/client_app.py similarity index 100% rename from e2e/bare-client-auth/client.py rename to e2e/e2e-bare-auth/e2e_bare_auth/client_app.py diff --git a/e2e/bare-https/server.py b/e2e/e2e-bare-auth/e2e_bare_auth/server_app.py similarity index 100% rename from e2e/bare-https/server.py rename to e2e/e2e-bare-auth/e2e_bare_auth/server_app.py diff --git a/e2e/bare-client-auth/generate.sh b/e2e/e2e-bare-auth/generate.sh similarity index 100% rename from e2e/bare-client-auth/generate.sh rename to e2e/e2e-bare-auth/generate.sh diff --git a/e2e/bare-client-auth/pyproject.toml b/e2e/e2e-bare-auth/pyproject.toml similarity index 73% rename from e2e/bare-client-auth/pyproject.toml rename to e2e/e2e-bare-auth/pyproject.toml index 49e566f93fd1..9b451c2ead99 100644 --- a/e2e/bare-client-auth/pyproject.toml +++ b/e2e/e2e-bare-auth/pyproject.toml @@ -3,9 +3,9 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-bare-auth" version = "1.0.0" -description = "Client-auth-enabled bare Federated Learning test with Flower" +description = "Auth-enabled bare Federated Learning test with Flower" license = "Apache-2.0" dependencies = [ "flwr @ {root:parent:parent:uri}", @@ -21,8 +21,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_bare_auth.server_app:app" +clientapp = "e2e_bare_auth.client_app:app" [tool.flwr.app.config] diff --git a/e2e/bare-https/README.md b/e2e/e2e-bare-https/README.md similarity index 100% rename from e2e/bare-https/README.md rename to e2e/e2e-bare-https/README.md diff --git a/e2e/bare-https/certificate.conf b/e2e/e2e-bare-https/certificate.conf similarity index 100% rename from e2e/bare-https/certificate.conf rename to e2e/e2e-bare-https/certificate.conf diff --git a/e2e/e2e-bare-https/e2e_bare_https/__init__.py b/e2e/e2e-bare-https/e2e_bare_https/__init__.py new file mode 100644 index 000000000000..473c050856ba --- /dev/null +++ b/e2e/e2e-bare-https/e2e_bare_https/__init__.py @@ -0,0 +1 @@ +"""bare_https_e2e.""" diff --git a/e2e/bare-https/client.py b/e2e/e2e-bare-https/e2e_bare_https/client_app.py similarity index 93% rename from e2e/bare-https/client.py rename to e2e/e2e-bare-https/e2e_bare_https/client_app.py index 4a682af3aec3..184978a30457 100644 --- a/e2e/bare-https/client.py +++ b/e2e/e2e-bare-https/e2e_bare_https/client_app.py @@ -39,6 +39,6 @@ def client_fn(context: Context): start_client( server_address="127.0.0.1:8080", client=FlowerClient().to_client(), - root_certificates=Path("certificates/ca.crt").read_bytes(), + root_certificates=Path("../certificates/ca.crt").read_bytes(), insecure=False, ) diff --git a/e2e/e2e-bare-https/e2e_bare_https/server_app.py b/e2e/e2e-bare-https/e2e_bare_https/server_app.py new file mode 100644 index 000000000000..cb466e703161 --- /dev/null +++ b/e2e/e2e-bare-https/e2e_bare_https/server_app.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import flwr as fl + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + hist = fl.server.start_server( + server_address="127.0.0.1:8080", + config=fl.server.ServerConfig(num_rounds=3), + certificates=( + Path("../certificates/ca.crt").read_bytes(), + Path("../certificates/server.pem").read_bytes(), + Path("../certificates/server.key").read_bytes(), + ), + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) diff --git a/e2e/bare-https/generate.sh b/e2e/e2e-bare-https/generate.sh similarity index 100% rename from e2e/bare-https/generate.sh rename to e2e/e2e-bare-https/generate.sh diff --git a/e2e/bare-https/pyproject.toml b/e2e/e2e-bare-https/pyproject.toml similarity index 83% rename from e2e/bare-https/pyproject.toml rename to e2e/e2e-bare-https/pyproject.toml index a9b69b8ed71d..0316e2b8402a 100644 --- a/e2e/bare-https/pyproject.toml +++ b/e2e/e2e-bare-https/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-bare-https" version = "1.0.0" description = "HTTPS-enabled bare Federated Learning test with Flower" license = "Apache-2.0" @@ -21,8 +21,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "server:app" -clientapp = "client:app" +serverapp = "e2e_bare_https.server_app:app" +clientapp = "e2e_bare_https.client_app:app" [tool.flwr.app.config] diff --git a/e2e/bare-https/simulation.py b/e2e/e2e-bare-https/simulation.py similarity index 100% rename from e2e/bare-https/simulation.py rename to e2e/e2e-bare-https/simulation.py diff --git a/e2e/bare/README.md b/e2e/e2e-bare/README.md similarity index 100% rename from e2e/bare/README.md rename to e2e/e2e-bare/README.md diff --git a/e2e/e2e-bare/e2e_bare/__init__.py b/e2e/e2e-bare/e2e_bare/__init__.py new file mode 100644 index 000000000000..5e7430011fc2 --- /dev/null +++ b/e2e/e2e-bare/e2e_bare/__init__.py @@ -0,0 +1 @@ +"""bare_e2e.""" diff --git a/e2e/bare/client.py b/e2e/e2e-bare/e2e_bare/client_app.py similarity index 100% rename from e2e/bare/client.py rename to e2e/e2e-bare/e2e_bare/client_app.py diff --git a/e2e/server.py b/e2e/e2e-bare/e2e_bare/server_app.py similarity index 100% rename from e2e/server.py rename to e2e/e2e-bare/e2e_bare/server_app.py diff --git a/e2e/bare/pyproject.toml b/e2e/e2e-bare/pyproject.toml similarity index 86% rename from e2e/bare/pyproject.toml rename to e2e/e2e-bare/pyproject.toml index 938cb66245ae..653d037a0192 100644 --- a/e2e/bare/pyproject.toml +++ b/e2e/e2e-bare/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-bare" version = "1.0.0" description = "Bare Federated Learning test with Flower" license = "Apache-2.0" @@ -21,8 +21,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_bare.server_app:app" +clientapp = "e2e_bare.client_app:app" [tool.flwr.app.config] diff --git a/e2e/bare/simulation.py b/e2e/e2e-bare/simulation.py similarity index 97% rename from e2e/bare/simulation.py rename to e2e/e2e-bare/simulation.py index 25868eb8e33f..fd41812eb3a8 100644 --- a/e2e/bare/simulation.py +++ b/e2e/e2e-bare/simulation.py @@ -1,7 +1,7 @@ from typing import List, Tuple import numpy as np -from client import client_fn +from e2e_bare.client_app import client_fn import flwr as fl from flwr.common import Metrics diff --git a/e2e/framework-fastai/README.md b/e2e/e2e-fastai/README.md similarity index 100% rename from e2e/framework-fastai/README.md rename to e2e/e2e-fastai/README.md diff --git a/e2e/e2e-fastai/e2e_fastai/__init__.py b/e2e/e2e-fastai/e2e_fastai/__init__.py new file mode 100644 index 000000000000..e64b144c6501 --- /dev/null +++ b/e2e/e2e-fastai/e2e_fastai/__init__.py @@ -0,0 +1 @@ +"""fastai_e2e.""" diff --git a/e2e/framework-fastai/client.py b/e2e/e2e-fastai/e2e_fastai/client_app.py similarity index 100% rename from e2e/framework-fastai/client.py rename to e2e/e2e-fastai/e2e_fastai/client_app.py diff --git a/e2e/e2e-fastai/e2e_fastai/server_app.py b/e2e/e2e-fastai/e2e_fastai/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-fastai/e2e_fastai/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-fastai/pyproject.toml b/e2e/e2e-fastai/pyproject.toml similarity index 86% rename from e2e/framework-fastai/pyproject.toml rename to e2e/e2e-fastai/pyproject.toml index 6157d0941bb5..58fecdabcc5d 100644 --- a/e2e/framework-fastai/pyproject.toml +++ b/e2e/e2e-fastai/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-fastai" version = "1.0.0" description = "Fastai Federated Learning E2E test with Flower" license = "Apache-2.0" @@ -23,8 +23,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_fastai.server_app:app" +clientapp = "e2e_fastai.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-fastai/simulation.py b/e2e/e2e-fastai/simulation.py similarity index 86% rename from e2e/framework-fastai/simulation.py rename to e2e/e2e-fastai/simulation.py index bf05a77cf32a..daf217d14765 100644 --- a/e2e/framework-fastai/simulation.py +++ b/e2e/e2e-fastai/simulation.py @@ -1,4 +1,4 @@ -from client import client_fn +from e2e_fastai.client_app import client_fn import flwr as fl diff --git a/e2e/framework-jax/README.md b/e2e/e2e-jax/README.md similarity index 100% rename from e2e/framework-jax/README.md rename to e2e/e2e-jax/README.md diff --git a/e2e/e2e-jax/e2e_jax/__init__.py b/e2e/e2e-jax/e2e_jax/__init__.py new file mode 100644 index 000000000000..18a53ad4b76c --- /dev/null +++ b/e2e/e2e-jax/e2e_jax/__init__.py @@ -0,0 +1 @@ +"""jax_e2e.""" diff --git a/e2e/framework-jax/client.py b/e2e/e2e-jax/e2e_jax/client_app.py similarity index 98% rename from e2e/framework-jax/client.py rename to e2e/e2e-jax/e2e_jax/client_app.py index c9ff67b3e38e..d834cf2b3610 100644 --- a/e2e/framework-jax/client.py +++ b/e2e/e2e-jax/e2e_jax/client_app.py @@ -3,8 +3,8 @@ from typing import Dict, List, Tuple import jax -import jax_training import numpy as np +from e2e_jax import jax_training from flwr.client import ClientApp, NumPyClient, start_client from flwr.common import Context diff --git a/e2e/framework-jax/jax_training.py b/e2e/e2e-jax/e2e_jax/jax_training.py similarity index 100% rename from e2e/framework-jax/jax_training.py rename to e2e/e2e-jax/e2e_jax/jax_training.py diff --git a/e2e/e2e-jax/e2e_jax/server_app.py b/e2e/e2e-jax/e2e_jax/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-jax/e2e_jax/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-jax/pyproject.toml b/e2e/e2e-jax/pyproject.toml similarity index 88% rename from e2e/framework-jax/pyproject.toml rename to e2e/e2e-jax/pyproject.toml index 1756526cfd55..b259f66a7bc3 100644 --- a/e2e/framework-jax/pyproject.toml +++ b/e2e/e2e-jax/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-jax" version = "1.0.0" description = "JAX example training a linear regression model with federated learning" license = "Apache-2.0" @@ -25,8 +25,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_jax.server_app:app" +clientapp = "e2e_jax.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-opacus/simulation.py b/e2e/e2e-jax/simulation.py similarity index 87% rename from e2e/framework-opacus/simulation.py rename to e2e/e2e-jax/simulation.py index bf05a77cf32a..105f5ab01a47 100644 --- a/e2e/framework-opacus/simulation.py +++ b/e2e/e2e-jax/simulation.py @@ -1,4 +1,4 @@ -from client import client_fn +from e2e_jax.client_app import client_fn import flwr as fl diff --git a/e2e/framework-opacus/.gitignore b/e2e/e2e-opacus/.gitignore similarity index 100% rename from e2e/framework-opacus/.gitignore rename to e2e/e2e-opacus/.gitignore diff --git a/e2e/framework-opacus/README.md b/e2e/e2e-opacus/README.md similarity index 100% rename from e2e/framework-opacus/README.md rename to e2e/e2e-opacus/README.md diff --git a/e2e/e2e-opacus/e2e_opacus/__init__.py b/e2e/e2e-opacus/e2e_opacus/__init__.py new file mode 100644 index 000000000000..e477387c100a --- /dev/null +++ b/e2e/e2e-opacus/e2e_opacus/__init__.py @@ -0,0 +1 @@ +"""opacus_e2e.""" diff --git a/e2e/framework-opacus/client.py b/e2e/e2e-opacus/e2e_opacus/client_app.py similarity index 98% rename from e2e/framework-opacus/client.py rename to e2e/e2e-opacus/e2e_opacus/client_app.py index 167fa4584e37..4077b44f5cd8 100644 --- a/e2e/framework-opacus/client.py +++ b/e2e/e2e-opacus/e2e_opacus/client_app.py @@ -79,7 +79,7 @@ def load_data(): transform = transforms.Compose( [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] ) - data = CIFAR10("./data", train=True, download=True, transform=transform) + data = CIFAR10("./../data", train=True, download=True, transform=transform) split = math.floor(len(data) * 0.01 * PARAMS["train_split"]) trainset = torch.utils.data.Subset(data, list(range(0, split))) testset = torch.utils.data.Subset( diff --git a/e2e/e2e-opacus/e2e_opacus/server_app.py b/e2e/e2e-opacus/e2e_opacus/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-opacus/e2e_opacus/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-opacus/pyproject.toml b/e2e/e2e-opacus/pyproject.toml similarity index 86% rename from e2e/framework-opacus/pyproject.toml rename to e2e/e2e-opacus/pyproject.toml index f54ea5dc95ef..54aa54a7b357 100644 --- a/e2e/framework-opacus/pyproject.toml +++ b/e2e/e2e-opacus/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-opacus" version = "1.0.0" description = "Opacus E2E testing" license = "Apache-2.0" @@ -24,8 +24,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_opacus.server_app:app" +clientapp = "e2e_opacus.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-jax/simulation.py b/e2e/e2e-opacus/simulation.py similarity index 86% rename from e2e/framework-jax/simulation.py rename to e2e/e2e-opacus/simulation.py index bf05a77cf32a..62d381f679c8 100644 --- a/e2e/framework-jax/simulation.py +++ b/e2e/e2e-opacus/simulation.py @@ -1,4 +1,4 @@ -from client import client_fn +from e2e_opacus.client_app import client_fn import flwr as fl diff --git a/e2e/framework-pandas/README.md b/e2e/e2e-pandas/README.md similarity index 100% rename from e2e/framework-pandas/README.md rename to e2e/e2e-pandas/README.md diff --git a/e2e/e2e-pandas/e2e_pandas/__init__.py b/e2e/e2e-pandas/e2e_pandas/__init__.py new file mode 100644 index 000000000000..608568ea7a62 --- /dev/null +++ b/e2e/e2e-pandas/e2e_pandas/__init__.py @@ -0,0 +1 @@ +"""pandas_e2e.""" diff --git a/e2e/framework-pandas/client.py b/e2e/e2e-pandas/e2e_pandas/client_app.py similarity index 87% rename from e2e/framework-pandas/client.py rename to e2e/e2e-pandas/e2e_pandas/client_app.py index 0c3300e1dd3f..98316988cd82 100644 --- a/e2e/framework-pandas/client.py +++ b/e2e/e2e-pandas/e2e_pandas/client_app.py @@ -6,7 +6,10 @@ from flwr.client import ClientApp, NumPyClient, start_client from flwr.common import Context -df = pd.read_csv("./data/client.csv") +try: + df = pd.read_csv("./../data/client.csv") # for new Flower +except FileNotFoundError: + df = pd.read_csv("./data/client.csv") # for legacy Flower column_names = ["sepal length (cm)", "sepal width (cm)"] diff --git a/e2e/framework-pandas/server.py b/e2e/e2e-pandas/e2e_pandas/server_app.py similarity index 96% rename from e2e/framework-pandas/server.py rename to e2e/e2e-pandas/e2e_pandas/server_app.py index 815cc9cee352..06f3eb68bb28 100644 --- a/e2e/framework-pandas/server.py +++ b/e2e/e2e-pandas/e2e_pandas/server_app.py @@ -1,4 +1,4 @@ -from strategy import FedAnalytics +from e2e_pandas.strategy import FedAnalytics import flwr as fl diff --git a/e2e/framework-pandas/strategy.py b/e2e/e2e-pandas/e2e_pandas/strategy.py similarity index 100% rename from e2e/framework-pandas/strategy.py rename to e2e/e2e-pandas/e2e_pandas/strategy.py diff --git a/e2e/framework-pandas/pyproject.toml b/e2e/e2e-pandas/pyproject.toml similarity index 88% rename from e2e/framework-pandas/pyproject.toml rename to e2e/e2e-pandas/pyproject.toml index 947b9bd27b76..f7d8f40264b3 100644 --- a/e2e/framework-pandas/pyproject.toml +++ b/e2e/e2e-pandas/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-pandas" version = "1.0.0" description = "Pandas E2E test with Flower" license = "Apache-2.0" @@ -30,8 +30,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "server:app" -clientapp = "client:app" +serverapp = "e2e_pandas.server_app:app" +clientapp = "e2e_pandas.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-pandas/simulation.py b/e2e/e2e-pandas/simulation.py similarity index 84% rename from e2e/framework-pandas/simulation.py rename to e2e/e2e-pandas/simulation.py index 8160fb744229..f34ff34a6a13 100644 --- a/e2e/framework-pandas/simulation.py +++ b/e2e/e2e-pandas/simulation.py @@ -1,5 +1,5 @@ -from client import client_fn -from strategy import FedAnalytics +from e2e_pandas.client_app import client_fn +from e2e_pandas.strategy import FedAnalytics import flwr as fl diff --git a/e2e/framework-pytorch-lightning/README.md b/e2e/e2e-pytorch-lightning/README.md similarity index 100% rename from e2e/framework-pytorch-lightning/README.md rename to e2e/e2e-pytorch-lightning/README.md diff --git a/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/__init__.py b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/__init__.py new file mode 100644 index 000000000000..7e10bceaa6b0 --- /dev/null +++ b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/__init__.py @@ -0,0 +1 @@ +"""pytorch_lightning_e2e.""" diff --git a/e2e/framework-pytorch-lightning/client.py b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/client_app.py similarity index 98% rename from e2e/framework-pytorch-lightning/client.py rename to e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/client_app.py index bf291a1ca2c5..3d2903037e85 100644 --- a/e2e/framework-pytorch-lightning/client.py +++ b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/client_app.py @@ -1,8 +1,8 @@ from collections import OrderedDict -import mnist import pytorch_lightning as pl import torch +from e2e_pytorch_lightning import mnist from flwr.client import ClientApp, NumPyClient, start_client from flwr.common import Context diff --git a/e2e/framework-pytorch-lightning/mnist.py b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/mnist.py similarity index 94% rename from e2e/framework-pytorch-lightning/mnist.py rename to e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/mnist.py index b23efc50d1e4..977a9ea524e8 100644 --- a/e2e/framework-pytorch-lightning/mnist.py +++ b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/mnist.py @@ -62,7 +62,7 @@ def _evaluate(self, batch, stage=None): def load_data(): # Training / validation set trainset = MNIST( - "./data", train=True, download=True, transform=transforms.ToTensor() + "./../data", train=True, download=True, transform=transforms.ToTensor() ) trainset = Subset(trainset, range(1000)) mnist_train, mnist_val = random_split(trainset, [800, 200]) @@ -71,7 +71,7 @@ def load_data(): # Test set testset = MNIST( - "./data", train=False, download=True, transform=transforms.ToTensor() + "./../data", train=False, download=True, transform=transforms.ToTensor() ) testset = Subset(testset, range(10)) test_loader = DataLoader(testset, batch_size=32, shuffle=False, num_workers=0) diff --git a/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/server_app.py b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-pytorch-lightning/e2e_pytorch_lightning/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-pytorch-lightning/pyproject.toml b/e2e/e2e-pytorch-lightning/pyproject.toml similarity index 83% rename from e2e/framework-pytorch-lightning/pyproject.toml rename to e2e/e2e-pytorch-lightning/pyproject.toml index 33d7a924dd8d..66ecbb6296d0 100644 --- a/e2e/framework-pytorch-lightning/pyproject.toml +++ b/e2e/e2e-pytorch-lightning/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-pytorch-lightning" version = "1.0.0" description = "Federated Learning E2E test with Flower and PyTorch Lightning" license = "Apache-2.0" @@ -23,8 +23,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_pytorch_lightning.server_app:app" +clientapp = "e2e_pytorch_lightning.client_app:app" [tool.flwr.app.config] diff --git a/e2e/e2e-pytorch-lightning/simulation.py b/e2e/e2e-pytorch-lightning/simulation.py new file mode 100644 index 000000000000..882498f7bba4 --- /dev/null +++ b/e2e/e2e-pytorch-lightning/simulation.py @@ -0,0 +1,14 @@ +from e2e_pytorch_lightning.client_app import client_fn + +import flwr as fl + +hist = fl.simulation.start_simulation( + client_fn=client_fn, + num_clients=2, + config=fl.server.ServerConfig(num_rounds=3), +) + +assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 +) diff --git a/e2e/framework-pytorch/README.md b/e2e/e2e-pytorch/README.md similarity index 100% rename from e2e/framework-pytorch/README.md rename to e2e/e2e-pytorch/README.md diff --git a/e2e/e2e-pytorch/e2e_pytorch/__init__.py b/e2e/e2e-pytorch/e2e_pytorch/__init__.py new file mode 100644 index 000000000000..96bfb84c0c09 --- /dev/null +++ b/e2e/e2e-pytorch/e2e_pytorch/__init__.py @@ -0,0 +1 @@ +"""pytorch_e2e.""" diff --git a/e2e/framework-pytorch/client.py b/e2e/e2e-pytorch/e2e_pytorch/client_app.py similarity index 96% rename from e2e/framework-pytorch/client.py rename to e2e/e2e-pytorch/e2e_pytorch/client_app.py index ab4bc7b5c5b9..988cd774018d 100644 --- a/e2e/framework-pytorch/client.py +++ b/e2e/e2e-pytorch/e2e_pytorch/client_app.py @@ -72,8 +72,8 @@ def test(net, testloader): def load_data(): """Load CIFAR-10 (training and test set).""" trf = Compose([ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) - trainset = CIFAR10("./data", train=True, download=True, transform=trf) - testset = CIFAR10("./data", train=False, download=True, transform=trf) + trainset = CIFAR10("./../data", train=True, download=True, transform=trf) + testset = CIFAR10("./../data", train=False, download=True, transform=trf) trainset = Subset(trainset, range(SUBSET_SIZE)) testset = Subset(testset, range(10)) return DataLoader(trainset, batch_size=32, shuffle=True), DataLoader(testset) diff --git a/e2e/e2e-pytorch/e2e_pytorch/server_app.py b/e2e/e2e-pytorch/e2e_pytorch/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-pytorch/e2e_pytorch/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-pytorch/pyproject.toml b/e2e/e2e-pytorch/pyproject.toml similarity index 86% rename from e2e/framework-pytorch/pyproject.toml rename to e2e/e2e-pytorch/pyproject.toml index 833b0fda26bc..0e48334693d3 100644 --- a/e2e/framework-pytorch/pyproject.toml +++ b/e2e/e2e-pytorch/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-pytorch" version = "1.0.0" description = "PyTorch Federated Learning E2E test with Flower" license = "Apache-2.0" @@ -24,8 +24,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_pytorch.server_app:app" +clientapp = "e2e_pytorch.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-pytorch/simulation.py b/e2e/e2e-pytorch/simulation.py similarity index 96% rename from e2e/framework-pytorch/simulation.py rename to e2e/e2e-pytorch/simulation.py index 25868eb8e33f..c465fbc4816e 100644 --- a/e2e/framework-pytorch/simulation.py +++ b/e2e/e2e-pytorch/simulation.py @@ -1,7 +1,7 @@ from typing import List, Tuple import numpy as np -from client import client_fn +from e2e_pytorch.client_app import client_fn import flwr as fl from flwr.common import Metrics diff --git a/e2e/framework-pytorch/simulation_next.py b/e2e/e2e-pytorch/simulation_next.py similarity index 82% rename from e2e/framework-pytorch/simulation_next.py rename to e2e/e2e-pytorch/simulation_next.py index ba1719dfb75b..01692a9e0850 100644 --- a/e2e/framework-pytorch/simulation_next.py +++ b/e2e/e2e-pytorch/simulation_next.py @@ -1,4 +1,4 @@ -from client import app as client_app +from e2e_pytorch.client_app import app as client_app import flwr as fl diff --git a/e2e/framework-scikit-learn/README.md b/e2e/e2e-scikit-learn/README.md similarity index 71% rename from e2e/framework-scikit-learn/README.md rename to e2e/e2e-scikit-learn/README.md index a3f4f74f8b0c..d880a113b9f4 100644 --- a/e2e/framework-scikit-learn/README.md +++ b/e2e/e2e-scikit-learn/README.md @@ -2,4 +2,4 @@ This directory is used for testing Flower with scikit-learn by using a simple logistic regression task. -It uses the `FedAvg` strategy with central evaluation. \ No newline at end of file +It uses the `FedAvg` strategy with central evaluation. diff --git a/e2e/e2e-scikit-learn/e2e_scikit_learn/__init__.py b/e2e/e2e-scikit-learn/e2e_scikit_learn/__init__.py new file mode 100644 index 000000000000..3e9806ba3387 --- /dev/null +++ b/e2e/e2e-scikit-learn/e2e_scikit_learn/__init__.py @@ -0,0 +1 @@ +"""scikit_learn_e2e.""" diff --git a/e2e/framework-scikit-learn/client.py b/e2e/e2e-scikit-learn/e2e_scikit_learn/client_app.py similarity index 98% rename from e2e/framework-scikit-learn/client.py rename to e2e/e2e-scikit-learn/e2e_scikit_learn/client_app.py index 24c6617c1289..ae00c240c9ba 100644 --- a/e2e/framework-scikit-learn/client.py +++ b/e2e/e2e-scikit-learn/e2e_scikit_learn/client_app.py @@ -1,7 +1,7 @@ import warnings import numpy as np -import utils +from e2e_scikit_learn import utils from sklearn.linear_model import LogisticRegression from sklearn.metrics import log_loss diff --git a/e2e/e2e-scikit-learn/e2e_scikit_learn/server_app.py b/e2e/e2e-scikit-learn/e2e_scikit_learn/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-scikit-learn/e2e_scikit_learn/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-scikit-learn/utils.py b/e2e/e2e-scikit-learn/e2e_scikit_learn/utils.py similarity index 100% rename from e2e/framework-scikit-learn/utils.py rename to e2e/e2e-scikit-learn/e2e_scikit_learn/utils.py diff --git a/e2e/framework-scikit-learn/pyproject.toml b/e2e/e2e-scikit-learn/pyproject.toml similarity index 87% rename from e2e/framework-scikit-learn/pyproject.toml rename to e2e/e2e-scikit-learn/pyproject.toml index 86e54f2f8a96..e14ea6ecc675 100644 --- a/e2e/framework-scikit-learn/pyproject.toml +++ b/e2e/e2e-scikit-learn/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-scikit-learn" version = "1.0.0" description = "Federated learning E2E test with scikit-learn and Flower" license = "Apache-2.0" @@ -27,8 +27,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_scikit_learn.server_app:app" +clientapp = "e2e_scikit_learn.client_app:app" [tool.flwr.app.config] diff --git a/e2e/e2e-scikit-learn/simulation.py b/e2e/e2e-scikit-learn/simulation.py new file mode 100644 index 000000000000..b0c9a3b58e7a --- /dev/null +++ b/e2e/e2e-scikit-learn/simulation.py @@ -0,0 +1,14 @@ +from e2e_scikit_learn.client_app import client_fn + +import flwr as fl + +hist = fl.simulation.start_simulation( + client_fn=client_fn, + num_clients=2, + config=fl.server.ServerConfig(num_rounds=3), +) + +assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 +) diff --git a/e2e/e2e-tensorflow/README.md b/e2e/e2e-tensorflow/README.md new file mode 100644 index 000000000000..15b6d8e0718d --- /dev/null +++ b/e2e/e2e-tensorflow/README.md @@ -0,0 +1,5 @@ +# Flower with Tensorflow testing + +This directory is used for testing Flower with Tensorflow by using the CIFAR10 dataset and a CNN. + +It uses a subset of size 1000 for the training data and 10 data points for the testing. diff --git a/e2e/e2e-tensorflow/e2e_tensorflow/__init__.py b/e2e/e2e-tensorflow/e2e_tensorflow/__init__.py new file mode 100644 index 000000000000..4a10173f8bd3 --- /dev/null +++ b/e2e/e2e-tensorflow/e2e_tensorflow/__init__.py @@ -0,0 +1 @@ +"""tensorflow_e2e.""" diff --git a/e2e/framework-tensorflow/client.py b/e2e/e2e-tensorflow/e2e_tensorflow/client_app.py similarity index 100% rename from e2e/framework-tensorflow/client.py rename to e2e/e2e-tensorflow/e2e_tensorflow/client_app.py diff --git a/e2e/e2e-tensorflow/e2e_tensorflow/server_app.py b/e2e/e2e-tensorflow/e2e_tensorflow/server_app.py new file mode 100644 index 000000000000..cb4f65eed0da --- /dev/null +++ b/e2e/e2e-tensorflow/e2e_tensorflow/server_app.py @@ -0,0 +1,79 @@ +import numpy as np + +import flwr as fl + +STATE_VAR = "timestamp" + + +# Define metric aggregation function +def record_state_metrics(metrics): + """Ensure that timestamps are monotonically increasing.""" + if not metrics: + return {} + + if STATE_VAR not in metrics[0][1]: + # Do nothing if keyword is not present + return {} + + states = [] + for _, m in metrics: + # split string and covert timestamps to float + states.append([float(tt) for tt in m[STATE_VAR].split(",")]) + + for client_state in states: + if len(client_state) == 1: + continue + deltas = np.diff(client_state) + assert np.all( + deltas > 0 + ), f"Timestamps are not monotonically increasing: {client_state}" + + return {STATE_VAR: states} + + +app = fl.server.ServerApp() + + +@app.main() +def main(driver, context): + # Construct the LegacyContext + context = fl.server.LegacyContext( + context=context, + config=fl.server.ServerConfig(num_rounds=3), + ) + + # Create the workflow + workflow = fl.server.workflow.DefaultWorkflow() + + # Execute + workflow(driver, context) + + hist = context.history + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + +if __name__ == "__main__": + strategy = fl.server.strategy.FedAvg( + evaluate_metrics_aggregation_fn=record_state_metrics + ) + + hist = fl.server.start_server( + server_address="0.0.0.0:8080", + config=fl.server.ServerConfig(num_rounds=3), + strategy=strategy, + ) + + assert ( + hist.losses_distributed[-1][1] == 0 + or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 + ) + + if STATE_VAR in hist.metrics_distributed: + # The checks in record_state_metrics don't do anythinng if client's state has a single entry + state_metrics_last_round = hist.metrics_distributed[STATE_VAR][-1] + assert ( + len(state_metrics_last_round[1][0]) == 2 * state_metrics_last_round[0] + ), "There should be twice as many entries in the client state as rounds" diff --git a/e2e/framework-tensorflow/pyproject.toml b/e2e/e2e-tensorflow/pyproject.toml similarity index 85% rename from e2e/framework-tensorflow/pyproject.toml rename to e2e/e2e-tensorflow/pyproject.toml index 944a9ec03651..dd89123944c7 100644 --- a/e2e/framework-tensorflow/pyproject.toml +++ b/e2e/e2e-tensorflow/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e-tensorflow" version = "1.0.0" description = "Keras Federated Learning E2E test with Flower" license = "Apache-2.0" @@ -23,8 +23,8 @@ allow-direct-references = true publisher = "flwrlabs" [tool.flwr.app.components] -serverapp = "" -clientapp = "client:app" +serverapp = "e2e_tensorflow.server_app:app" +clientapp = "e2e_tensorflow.client_app:app" [tool.flwr.app.config] diff --git a/e2e/framework-pytorch-lightning/simulation.py b/e2e/e2e-tensorflow/simulation.py similarity index 85% rename from e2e/framework-pytorch-lightning/simulation.py rename to e2e/e2e-tensorflow/simulation.py index bf05a77cf32a..108be2ae10be 100644 --- a/e2e/framework-pytorch-lightning/simulation.py +++ b/e2e/e2e-tensorflow/simulation.py @@ -1,4 +1,4 @@ -from client import client_fn +from e2e_tensorflow.client_app import client_fn import flwr as fl diff --git a/e2e/framework-tensorflow/simulation_next.py b/e2e/e2e-tensorflow/simulation_next.py similarity index 81% rename from e2e/framework-tensorflow/simulation_next.py rename to e2e/e2e-tensorflow/simulation_next.py index ba1719dfb75b..0fd75d8cd0ec 100644 --- a/e2e/framework-tensorflow/simulation_next.py +++ b/e2e/e2e-tensorflow/simulation_next.py @@ -1,4 +1,4 @@ -from client import app as client_app +from e2e_tensorflow.client_app import app as client_app import flwr as fl diff --git a/e2e/framework-scikit-learn/simulation.py b/e2e/framework-scikit-learn/simulation.py deleted file mode 100644 index bf05a77cf32a..000000000000 --- a/e2e/framework-scikit-learn/simulation.py +++ /dev/null @@ -1,14 +0,0 @@ -from client import client_fn - -import flwr as fl - -hist = fl.simulation.start_simulation( - client_fn=client_fn, - num_clients=2, - config=fl.server.ServerConfig(num_rounds=3), -) - -assert ( - hist.losses_distributed[-1][1] == 0 - or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 -) diff --git a/e2e/framework-tensorflow/README.md b/e2e/framework-tensorflow/README.md deleted file mode 100644 index 7d8a245a3fb0..000000000000 --- a/e2e/framework-tensorflow/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Flower with PyTorch testing - -This directory is used for testing Flower with PyTorch by using the CIFAR10 dataset and a CNN. - -It uses a subset of size 1000 for the training data and 10 data points for the testing. diff --git a/e2e/framework-tensorflow/simulation.py b/e2e/framework-tensorflow/simulation.py deleted file mode 100644 index bf05a77cf32a..000000000000 --- a/e2e/framework-tensorflow/simulation.py +++ /dev/null @@ -1,14 +0,0 @@ -from client import client_fn - -import flwr as fl - -hist = fl.simulation.start_simulation( - client_fn=client_fn, - num_clients=2, - config=fl.server.ServerConfig(num_rounds=3), -) - -assert ( - hist.losses_distributed[-1][1] == 0 - or (hist.losses_distributed[0][1] / hist.losses_distributed[-1][1]) >= 0.98 -) diff --git a/e2e/pyproject.toml b/e2e/pyproject.toml index dd34a693ab3a..0fc4c77f44b2 100644 --- a/e2e/pyproject.toml +++ b/e2e/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "e2e_test" +name = "e2e" version = "1.0.0" description = "Project configuration for ServerApp in E2E tests." license = "Apache-2.0" diff --git a/e2e/test_legacy.sh b/e2e/test_legacy.sh index dc17ca8c6378..b336ee0cb717 100755 --- a/e2e/test_legacy.sh +++ b/e2e/test_legacy.sh @@ -1,28 +1,19 @@ #!/bin/bash set -e -case "$1" in - framework-pandas) - server_file="server.py" - ;; - bare-https) - ./generate.sh - server_file="server.py" - ;; - *) - server_file="../server.py" - ;; -esac +if [ "$1" = "e2e-bare-https" ]; then + ./../generate.sh +fi # run the first command in background and save output to a temporary file: -timeout 2m python $server_file & +timeout 2m python server_app.py & pid=$! sleep 3 -python client.py & +python client_app.py & sleep 3 -python client.py & +python client_app.py & sleep 3 wait $pid diff --git a/e2e/test_reconnection.sh b/e2e/test_reconnection.sh index 49f25507f047..80788b92ebde 100755 --- a/e2e/test_reconnection.sh +++ b/e2e/test_reconnection.sh @@ -29,12 +29,12 @@ sl_pid=$! echo "Starting SuperLink" sleep 3 -timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & +timeout 2m flower-supernode ./ --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting first client" sleep 3 -timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & +timeout 2m flower-supernode ./ --insecure $rest_arg --superlink $server_address & cl2_pid=$! echo "Starting second client" sleep 3 @@ -56,13 +56,13 @@ echo "Killing first client" sleep 3 # Starting new client, this is so we have enough clients to start the server-app -timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & +timeout 2m flower-supernode ./ --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting new client" sleep 5 # We start the server-app to begining the training -timeout 2m flower-server-app ./.. $rest_arg --superlink $server_app_address & +timeout 2m flower-server-app ./ $rest_arg --superlink $server_app_address & pid=$! echo "Starting server-app to start training" @@ -74,7 +74,7 @@ echo "Killing first client" sleep 1 # Restart first client so enough clients are connected to continue the FL rounds -timeout 2m flower-supernode . --insecure $rest_arg --superlink $server_address & +timeout 2m flower-supernode ./ --insecure $rest_arg --superlink $server_address & cl1_pid=$! echo "Starting new client" diff --git a/e2e/test_superlink.sh b/e2e/test_superlink.sh index a6ee03af7746..684f386bd388 100755 --- a/e2e/test_superlink.sh +++ b/e2e/test_superlink.sh @@ -2,12 +2,7 @@ set -e case "$1" in - framework-pandas) - server_arg="--insecure" - client_arg="--insecure" - server_dir="./" - ;; - bare-https) + e2e-bare-https) ./generate.sh server_arg="--ssl-ca-certfile certificates/ca.crt --ssl-certfile certificates/server.pem --ssl-keyfile certificates/server.key" client_arg="--root-certificates certificates/ca.crt" @@ -16,7 +11,7 @@ case "$1" in *) server_arg="--insecure" client_arg="--insecure" - server_dir="./.." + server_dir="./" ;; esac From b5109e0f43cdefe633fafd4b510d4bb2c8b38cf0 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 16 Aug 2024 21:08:11 +0200 Subject: [PATCH 079/188] fix(framework) Fix end-to-end FAB delivery (#4029) --- src/py/flwr/cli/config_utils.py | 4 ++-- .../server/superlink/driver/driver_servicer.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/cli/config_utils.py b/src/py/flwr/cli/config_utils.py index 5e5f56555da4..233d35a5fa17 100644 --- a/src/py/flwr/cli/config_utils.py +++ b/src/py/flwr/cli/config_utils.py @@ -74,13 +74,13 @@ def get_fab_metadata(fab_file: Union[Path, bytes]) -> Tuple[str, str]: Returns ------- Tuple[str, str] - The `fab_version` and `fab_id` of the given Flower App Bundle. + The `fab_id` and `fab_version` of the given Flower App Bundle. """ conf = get_fab_config(fab_file) return ( - conf["project"]["version"], f"{conf['tool']['flwr']['app']['publisher']}/{conf['project']['name']}", + conf["project"]["version"], ) diff --git a/src/py/flwr/server/superlink/driver/driver_servicer.py b/src/py/flwr/server/superlink/driver/driver_servicer.py index 14e88d08da5b..73cd1c73a6fd 100644 --- a/src/py/flwr/server/superlink/driver/driver_servicer.py +++ b/src/py/flwr/server/superlink/driver/driver_servicer.py @@ -23,7 +23,12 @@ import grpc from flwr.common.logger import log -from flwr.common.serde import fab_to_proto, user_config_from_proto, user_config_to_proto +from flwr.common.serde import ( + fab_from_proto, + fab_to_proto, + user_config_from_proto, + user_config_to_proto, +) from flwr.common.typing import Fab from flwr.proto import driver_pb2_grpc # pylint: disable=E0611 from flwr.proto.driver_pb2 import ( # pylint: disable=E0611 @@ -75,12 +80,13 @@ def CreateRun( """Create run ID.""" log(DEBUG, "DriverServicer.CreateRun") state: State = self.state_factory.state() - if request.HasField("fab") and request.fab.HasField("content"): + if request.HasField("fab"): + fab = fab_from_proto(request.fab) ffs: Ffs = self.ffs_factory.ffs() - fab_hash = ffs.put(request.fab.content, {}) + fab_hash = ffs.put(fab.content, {}) _raise_if( - fab_hash != request.fab.hash_str, - f"FAB ({request.fab}) hash from request doesn't match contents", + fab_hash != fab.hash_str, + f"FAB ({fab.hash_str}) hash from request doesn't match contents", ) else: fab_hash = "" From b979e2371e2a54b21550358e541a0b1e09b0993e Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Fri, 16 Aug 2024 21:14:18 +0200 Subject: [PATCH 080/188] fix(framework:skip) Update run with `fab_id` and `fab_version` (#4031) --- src/py/flwr/client/app.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index bc42075aa9e5..2847f6c2e5c4 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -412,8 +412,12 @@ def _on_backoff(retry_state: RetryState) -> None: if get_fab is not None and run.fab_hash: fab = get_fab(run.fab_hash) install_from_fab(fab.content, flwr_path, True) + fab_id, fab_version = get_fab_metadata(fab.content) else: fab = None + fab_id, fab_version = run.fab_id, run.fab_version + + run.fab_id, run.fab_version = fab_id, fab_version # Register context for this run node_state.register_context( @@ -432,11 +436,6 @@ def _on_backoff(retry_state: RetryState) -> None: # Handle app loading and task message try: # Load ClientApp instance - if fab: - fab_id, fab_version = get_fab_metadata(fab.content) - else: - fab_id, fab_version = run.fab_id, run.fab_version - client_app: ClientApp = load_client_app_fn(fab_id, fab_version) # Execute ClientApp From e9e45f323514e88918afe9544831f57d887bc195 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 16 Aug 2024 21:05:48 +0100 Subject: [PATCH 081/188] feat(framework) Add `ClientApp` multiprocess execution (#3978) Co-authored-by: Daniel J. Beutel Co-authored-by: Javier --- src/py/flwr/client/app.py | 66 ++++++++++++++++--- .../client/process/clientappio_servicer.py | 11 ++-- src/py/flwr/client/process/process.py | 6 +- src/py/flwr/client/supernode/app.py | 19 +++++- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 2847f6c2e5c4..e324d6c2b824 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -15,6 +15,7 @@ """Flower client app.""" import signal +import subprocess import sys import time from dataclasses import dataclass @@ -35,6 +36,7 @@ from flwr.common.address import parse_address from flwr.common.constant import ( MISSING_EXTRA_REST, + RUN_ID_NUM_BYTES, TRANSPORT_TYPE_GRPC_ADAPTER, TRANSPORT_TYPE_GRPC_BIDI, TRANSPORT_TYPE_GRPC_RERE, @@ -48,6 +50,7 @@ from flwr.common.typing import Fab, Run, UserConfig from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server +from flwr.server.superlink.state.utils import generate_rand_int_from_bytes from .grpc_adapter_client.connection import grpc_adapter from .grpc_client.connection import grpc_connection @@ -55,7 +58,7 @@ from .message_handler.message_handler import handle_control_message from .node_state import NodeState from .numpy_client import NumPyClient -from .process.clientappio_servicer import ClientAppIoServicer +from .process.clientappio_servicer import ClientAppIoInputs, ClientAppIoServicer ADDRESS_CLIENTAPPIO_API_GRPC_RERE = "0.0.0.0:9094" @@ -204,6 +207,8 @@ def start_client_internal( max_retries: Optional[int] = None, max_wait_time: Optional[float] = None, flwr_path: Optional[Path] = None, + isolate: Optional[bool] = False, + supernode_address: Optional[str] = ADDRESS_CLIENTAPPIO_API_GRPC_RERE, ) -> None: """Start a Flower client node which connects to a Flower server. @@ -251,6 +256,13 @@ class `flwr.client.Client` (default: None) If set to None, there is no limit to the total time. flwr_path: Optional[Path] (default: None) The fully resolved path containing installed Flower Apps. + isolate : Optional[bool] (default: False) + Whether to run `ClientApp` in a separate process. By default, this value is + `False`, and the `ClientApp` runs in the same process as the SuperNode. If + `True`, the `ClientApp` runs in an isolated process and communicates using + gRPC at the address `supernode_address`. + supernode_address : Optional[str] (default: `ADDRESS_CLIENTAPPIO_API_GRPC_RERE`) + The SuperNode gRPC server address. """ if insecure is None: insecure = root_certificates is None @@ -276,6 +288,13 @@ def _load_client_app(_1: str, _2: str) -> ClientApp: load_client_app_fn = _load_client_app + if isolate: + if supernode_address is None: + raise ValueError("`supernode_address` required when `isolate` is set") + _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc( + address=supernode_address + ) + # At this point, only `load_client_app_fn` should be used # Both `client` and `client_fn` must not be used directly @@ -435,11 +454,44 @@ def _on_backoff(retry_state: RetryState) -> None: # Handle app loading and task message try: - # Load ClientApp instance - client_app: ClientApp = load_client_app_fn(fab_id, fab_version) + if isolate and supernode_address is not None: + # Generate SuperNode token + token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) + + # Share Message and Context with servicer + clientappio_servicer.set_inputs( + ClientAppIoInputs( + message=message, + context=context, + run=run, + token=token, + ) + ) + + # Run `ClientApp` in subprocess + command = [ + "flwr-clientapp", + "--supernode", + supernode_address, + "--token", + str(token), + ] + subprocess.run( + command, + stdout=None, + stderr=None, + check=True, + ) + outputs = clientappio_servicer.get_outputs() + reply_message, context = outputs.message, outputs.context + else: + # Load ClientApp instance + client_app: ClientApp = load_client_app_fn( + fab_id, fab_version + ) - # Execute ClientApp - reply_message = client_app(message=message, context=context) + # Execute ClientApp + reply_message = client_app(message=message, context=context) except Exception as ex: # pylint: disable=broad-exception-caught # Legacy grpc-bidi @@ -685,9 +737,7 @@ def signal_handler(sig, frame): # type: ignore signal.signal(signal.SIGTERM, signal_handler) -def run_clientappio_api_grpc( - address: str = ADDRESS_CLIENTAPPIO_API_GRPC_RERE, -) -> Tuple[grpc.Server, ClientAppIoServicer]: +def run_clientappio_api_grpc(address: str) -> Tuple[grpc.Server, ClientAppIoServicer]: """Run ClientAppIo API gRPC server.""" clientappio_servicer: grpc.Server = ClientAppIoServicer() clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server diff --git a/src/py/flwr/client/process/clientappio_servicer.py b/src/py/flwr/client/process/clientappio_servicer.py index f614fadf8070..9f009f2c8a9f 100644 --- a/src/py/flwr/client/process/clientappio_servicer.py +++ b/src/py/flwr/client/process/clientappio_servicer.py @@ -94,10 +94,6 @@ def PushClientAppOutputs( ) -> PushClientAppOutputsResponse: """Push Message and Context.""" log(DEBUG, "ClientAppIo.PushClientAppOutputs") - if self.clientapp_output is None: - raise ValueError( - "ClientAppIoOutputs not set before calling `PushClientAppOutputs`." - ) if self.clientapp_input is None: raise ValueError( "ClientAppIoInputs not set before calling `PushClientAppOutputs`." @@ -109,8 +105,11 @@ def PushClientAppOutputs( ) try: # Update Message and Context - self.clientapp_output.message = message_from_proto(request.message) - self.clientapp_output.context = context_from_proto(request.context) + self.clientapp_output = ClientAppIoOutputs( + message=message_from_proto(request.message), + context=context_from_proto(request.context), + ) + # Set status code = typing.ClientAppOutputCode.SUCCESS status = typing.ClientAppOutputStatus(code=code, message="Success") diff --git a/src/py/flwr/client/process/process.py b/src/py/flwr/client/process/process.py index 91a9670e8658..18766d2dd082 100644 --- a/src/py/flwr/client/process/process.py +++ b/src/py/flwr/client/process/process.py @@ -52,20 +52,20 @@ def on_channel_state_change(channel_connectivity: str) -> None: def run_clientapp( # pylint: disable=R0914 - address: str, + supernode: str, token: int, ) -> None: """Run Flower ClientApp process. Parameters ---------- - address : str + supernode : str Address of SuperNode token : int Unique SuperNode token for ClientApp-SuperNode authentication """ channel = create_channel( - server_address=address, + server_address=supernode, insecure=True, ) channel.subscribe(on_channel_state_change) diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 4370d7d1219d..b299aa468390 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -73,6 +73,8 @@ def run_supernode() -> None: max_retries=args.max_retries, max_wait_time=args.max_wait_time, node_config=parse_config_args([args.node_config]), + isolate=args.isolate, + supernode_address=args.supernode_address, ) # Graceful shutdown @@ -121,7 +123,7 @@ def flwr_clientapp() -> None: description="Run a Flower ClientApp", ) parser.add_argument( - "--address", + "--supernode", help="Address of SuperNode ClientAppIo gRPC servicer", ) parser.add_argument( @@ -133,10 +135,10 @@ def flwr_clientapp() -> None: DEBUG, "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s " "with the token %s", - args.address, + args.supernode, args.token, ) - run_clientapp(address=args.address, token=int(args.token)) + run_clientapp(supernode=args.supernode, token=int(args.token)) def _warn_deprecated_server_arg(args: argparse.Namespace) -> None: @@ -223,6 +225,17 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser: - `$HOME/.flwr/` in all other cases """, ) + parser.add_argument( + "--isolate", + action="store_true", + help="Run `ClientApp` in an isolated subprocess. By default, `ClientApp` " + "runs in the same process that executes the SuperNode.", + ) + parser.add_argument( + "--supernode-address", + default="0.0.0.0:9094", + help="Set the SuperNode gRPC server address. Defaults to `0.0.0.0:9094`.", + ) return parser From a4b8f74f1f86c8e4f7560cca5f6aeee443d572e1 Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 17 Aug 2024 09:45:18 +0100 Subject: [PATCH 082/188] refactor(framework) Rename `client.process` to `client.clientapp` (#4032) --- src/py/flwr/client/app.py | 2 +- src/py/flwr/client/{process => clientapp}/__init__.py | 0 src/py/flwr/client/{process/process.py => clientapp/app.py} | 0 .../client/{process => clientapp}/clientappio_servicer.py | 0 .../{process => clientapp}/clientappio_servicer_test.py | 4 ++-- src/py/flwr/client/{process => clientapp}/utils.py | 0 src/py/flwr/client/supernode/app.py | 4 ++-- src/py/flwr/server/superlink/fleet/vce/vce_api.py | 2 +- 8 files changed, 6 insertions(+), 6 deletions(-) rename src/py/flwr/client/{process => clientapp}/__init__.py (100%) rename src/py/flwr/client/{process/process.py => clientapp/app.py} (100%) rename src/py/flwr/client/{process => clientapp}/clientappio_servicer.py (100%) rename src/py/flwr/client/{process => clientapp}/clientappio_servicer_test.py (97%) rename src/py/flwr/client/{process => clientapp}/utils.py (100%) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index e324d6c2b824..8b7cab79ef54 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -52,13 +52,13 @@ from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server from flwr.server.superlink.state.utils import generate_rand_int_from_bytes +from .clientapp.clientappio_servicer import ClientAppIoInputs, ClientAppIoServicer from .grpc_adapter_client.connection import grpc_adapter from .grpc_client.connection import grpc_connection from .grpc_rere_client.connection import grpc_request_response from .message_handler.message_handler import handle_control_message from .node_state import NodeState from .numpy_client import NumPyClient -from .process.clientappio_servicer import ClientAppIoInputs, ClientAppIoServicer ADDRESS_CLIENTAPPIO_API_GRPC_RERE = "0.0.0.0:9094" diff --git a/src/py/flwr/client/process/__init__.py b/src/py/flwr/client/clientapp/__init__.py similarity index 100% rename from src/py/flwr/client/process/__init__.py rename to src/py/flwr/client/clientapp/__init__.py diff --git a/src/py/flwr/client/process/process.py b/src/py/flwr/client/clientapp/app.py similarity index 100% rename from src/py/flwr/client/process/process.py rename to src/py/flwr/client/clientapp/app.py diff --git a/src/py/flwr/client/process/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py similarity index 100% rename from src/py/flwr/client/process/clientappio_servicer.py rename to src/py/flwr/client/clientapp/clientappio_servicer.py diff --git a/src/py/flwr/client/process/clientappio_servicer_test.py b/src/py/flwr/client/clientapp/clientappio_servicer_test.py similarity index 97% rename from src/py/flwr/client/process/clientappio_servicer_test.py rename to src/py/flwr/client/clientapp/clientappio_servicer_test.py index 4831825acf30..f2efcef14e86 100644 --- a/src/py/flwr/client/process/clientappio_servicer_test.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer_test.py @@ -17,7 +17,7 @@ import unittest from unittest.mock import Mock, patch -from flwr.client.process.process import pull_message, push_message +from flwr.client.clientapp.app import pull_message, push_message from flwr.common import Context, Message, typing from flwr.common.serde import ( clientappstatus_from_proto, @@ -50,7 +50,7 @@ def setUp(self) -> None: self.maker = RecordMaker() self.mock_stub = Mock() self.patcher = patch( - "flwr.client.process.process.ClientAppIoStub", return_value=self.mock_stub + "flwr.client.clientapp.app.ClientAppIoStub", return_value=self.mock_stub ) self.patcher.start() diff --git a/src/py/flwr/client/process/utils.py b/src/py/flwr/client/clientapp/utils.py similarity index 100% rename from src/py/flwr/client/process/utils.py rename to src/py/flwr/client/clientapp/utils.py diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index b299aa468390..0f8d9ab675b4 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -38,8 +38,8 @@ from flwr.common.logger import log, warn_deprecated_feature from ..app import start_client_internal -from ..process.process import run_clientapp -from ..process.utils import get_load_client_app_fn +from ..clientapp.app import run_clientapp +from ..clientapp.utils import get_load_client_app_fn ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092" diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index aa726f902d47..81eb6cb6569c 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -27,8 +27,8 @@ from typing import Callable, Dict, Optional from flwr.client.client_app import ClientApp, ClientAppException, LoadClientAppError +from flwr.client.clientapp.utils import get_load_client_app_fn from flwr.client.node_state import NodeState -from flwr.client.process.utils import get_load_client_app_fn from flwr.common.constant import ( NUM_PARTITIONS_KEY, PARTITION_ID_KEY, From 4ea34ff3d9ff51c1091eef191c4cb62d446f9a4f Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 17 Aug 2024 11:06:02 +0100 Subject: [PATCH 083/188] refactor(framework:skip) Update the `flwr-clientapp` entry point from CLI (#4033) --- pyproject.toml | 2 +- src/py/flwr/client/clientapp/__init__.py | 7 ++++++ src/py/flwr/client/clientapp/app.py | 27 ++++++++++++++++++++++++ src/py/flwr/client/supernode/__init__.py | 2 -- src/py/flwr/client/supernode/app.py | 27 ------------------------ 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1369ee4e968a..0d0138a5689b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ flower-supernode = "flwr.client:run_supernode" flower-client-app = "flwr.client:run_client_app" flower-server-app = "flwr.server:run_server_app" flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" -flwr-clientapp = "flwr.client.supernode:flwr_clientapp" +flwr-clientapp = "flwr.client.clientapp:flwr_clientapp" [tool.poetry.dependencies] python = "^3.8" diff --git a/src/py/flwr/client/clientapp/__init__.py b/src/py/flwr/client/clientapp/__init__.py index 653cee434c12..e877ee22db16 100644 --- a/src/py/flwr/client/clientapp/__init__.py +++ b/src/py/flwr/client/clientapp/__init__.py @@ -13,3 +13,10 @@ # limitations under the License. # ============================================================================== """Flower AppIO service.""" + + +from .app import flwr_clientapp as flwr_clientapp + +__all__ = [ + "flwr_clientapp", +] diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index 18766d2dd082..18602a4bb7ab 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -14,6 +14,7 @@ # ============================================================================== """Flower ClientApp process.""" +import argparse from logging import DEBUG, ERROR, INFO from typing import Tuple @@ -46,6 +47,32 @@ from .utils import get_load_client_app_fn +def flwr_clientapp() -> None: + """Run process-isolated Flower ClientApp.""" + log(INFO, "Starting Flower ClientApp") + + parser = argparse.ArgumentParser( + description="Run a Flower ClientApp", + ) + parser.add_argument( + "--supernode", + help="Address of SuperNode ClientAppIo gRPC servicer", + ) + parser.add_argument( + "--token", + help="Unique token generated by SuperNode for each ClientApp execution", + ) + args = parser.parse_args() + log( + DEBUG, + "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s " + "with the token %s", + args.supernode, + args.token, + ) + run_clientapp(supernode=args.supernode, token=int(args.token)) + + def on_channel_state_change(channel_connectivity: str) -> None: """Log channel connectivity.""" log(DEBUG, channel_connectivity) diff --git a/src/py/flwr/client/supernode/__init__.py b/src/py/flwr/client/supernode/__init__.py index 128d0286d625..bc505f693875 100644 --- a/src/py/flwr/client/supernode/__init__.py +++ b/src/py/flwr/client/supernode/__init__.py @@ -15,12 +15,10 @@ """Flower SuperNode.""" -from .app import flwr_clientapp as flwr_clientapp from .app import run_client_app as run_client_app from .app import run_supernode as run_supernode __all__ = [ - "flwr_clientapp", "run_client_app", "run_supernode", ] diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 0f8d9ab675b4..ef17b9375d70 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -38,7 +38,6 @@ from flwr.common.logger import log, warn_deprecated_feature from ..app import start_client_internal -from ..clientapp.app import run_clientapp from ..clientapp.utils import get_load_client_app_fn ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092" @@ -115,32 +114,6 @@ def run_client_app() -> None: register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE) -def flwr_clientapp() -> None: - """Run process-isolated Flower ClientApp.""" - log(INFO, "Starting Flower ClientApp") - - parser = argparse.ArgumentParser( - description="Run a Flower ClientApp", - ) - parser.add_argument( - "--supernode", - help="Address of SuperNode ClientAppIo gRPC servicer", - ) - parser.add_argument( - "--token", - help="Unique token generated by SuperNode for each ClientApp execution", - ) - args = parser.parse_args() - log( - DEBUG, - "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s " - "with the token %s", - args.supernode, - args.token, - ) - run_clientapp(supernode=args.supernode, token=int(args.token)) - - def _warn_deprecated_server_arg(args: argparse.Namespace) -> None: """Warn about the deprecated argument `--server`.""" if args.server != ADDRESS_FLEET_API_GRPC_RERE: From 542b71c25d80b533ac295fb2decbe2077317c698 Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 17 Aug 2024 20:17:43 +0100 Subject: [PATCH 084/188] feat(framework) Add `GetToken` gRPC request to `ClientAppIoServicer` (#4034) Co-authored-by: Daniel J. Beutel --- src/proto/flwr/proto/clientappio.proto | 7 + src/py/flwr/client/app.py | 5 +- src/py/flwr/client/clientapp/app.py | 10 +- .../client/clientapp/clientappio_servicer.py | 136 +++++++++++++++--- .../clientapp/clientappio_servicer_test.py | 27 +++- src/py/flwr/proto/clientappio_pb2.py | 30 ++-- src/py/flwr/proto/clientappio_pb2.pyi | 17 +++ src/py/flwr/proto/clientappio_pb2_grpc.py | 34 +++++ src/py/flwr/proto/clientappio_pb2_grpc.pyi | 13 ++ 9 files changed, 237 insertions(+), 42 deletions(-) diff --git a/src/proto/flwr/proto/clientappio.proto b/src/proto/flwr/proto/clientappio.proto index d73ed086f40d..f4f52184634e 100644 --- a/src/proto/flwr/proto/clientappio.proto +++ b/src/proto/flwr/proto/clientappio.proto @@ -7,6 +7,9 @@ import "flwr/proto/run.proto"; import "flwr/proto/message.proto"; service ClientAppIo { + // Get token + rpc GetToken(GetTokenRequest) returns (GetTokenResponse) {} + // Get Message, Context, and Run rpc PullClientAppInputs(PullClientAppInputsRequest) returns (PullClientAppInputsResponse) {} @@ -26,12 +29,16 @@ message ClientAppOutputStatus { string message = 2; } +message GetTokenRequest {} +message GetTokenResponse { sint64 token = 1; } + message PullClientAppInputsRequest { sint64 token = 1; } message PullClientAppInputsResponse { Message message = 1; Context context = 2; Run run = 3; } + message PushClientAppOutputsRequest { sint64 token = 1; Message message = 2; diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 8b7cab79ef54..fbade8bbfb22 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -460,12 +460,13 @@ def _on_backoff(retry_state: RetryState) -> None: # Share Message and Context with servicer clientappio_servicer.set_inputs( - ClientAppIoInputs( + clientapp_input=ClientAppIoInputs( message=message, context=context, run=run, token=token, - ) + ), + token_returned=True, ) # Run `ClientApp` in subprocess diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index 18602a4bb7ab..03a3a84af030 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -16,7 +16,7 @@ import argparse from logging import DEBUG, ERROR, INFO -from typing import Tuple +from typing import Optional, Tuple import grpc @@ -37,6 +37,8 @@ # pylint: disable=E0611 from flwr.proto.clientappio_pb2 import ( + GetTokenRequest, + GetTokenResponse, PullClientAppInputsRequest, PullClientAppInputsResponse, PushClientAppOutputsRequest, @@ -145,6 +147,12 @@ def run_clientapp( # pylint: disable=R0914 channel.close() +def get_token(stub: grpc.Channel) -> Optional[int]: + """Get a token from SuperNode.""" + res: GetTokenResponse = stub.GetToken(GetTokenRequest()) + return res.token + + def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run]: """Pull message from SuperNode to ClientApp.""" res: PullClientAppInputsResponse = stub.PullClientAppInputs( diff --git a/src/py/flwr/client/clientapp/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py index 9f009f2c8a9f..3483fe8fa8f7 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer.py @@ -17,7 +17,7 @@ from dataclasses import dataclass from logging import DEBUG, ERROR -from typing import Optional +from typing import Optional, cast import grpc @@ -36,6 +36,8 @@ # pylint: disable=E0611 from flwr.proto import clientappio_pb2_grpc from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0401 + GetTokenRequest, + GetTokenResponse, PullClientAppInputsRequest, PullClientAppInputsResponse, PushClientAppOutputsRequest, @@ -68,25 +70,72 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer): def __init__(self) -> None: self.clientapp_input: Optional[ClientAppIoInputs] = None self.clientapp_output: Optional[ClientAppIoOutputs] = None + self.token_returned: bool = False + self.inputs_returned: bool = False + + def GetToken( + self, request: GetTokenRequest, context: grpc.ServicerContext + ) -> GetTokenResponse: + """Get token.""" + log(DEBUG, "ClientAppIo.GetToken") + + # Fail if no ClientAppIoInputs are available + if self.clientapp_input is None: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "No inputs available.", + ) + clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + + # Fail if token was already returned in a previous call + if self.token_returned: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "Token already returned. A token can be returned only once.", + ) + + # If + # - ClientAppIoInputs is set, and + # - token hasn't been returned before, + # return token + self.token_returned = True + return GetTokenResponse(token=clientapp_input.token) def PullClientAppInputs( self, request: PullClientAppInputsRequest, context: grpc.ServicerContext ) -> PullClientAppInputsResponse: """Pull Message, Context, and Run.""" log(DEBUG, "ClientAppIo.PullClientAppInputs") + + # Fail if no ClientAppIoInputs are available if self.clientapp_input is None: - raise ValueError( - "ClientAppIoInputs not set before calling `PullClientAppInputs`." + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "No inputs available.", ) - if request.token != self.clientapp_input.token: + clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + + # Fail if token wasn't returned in a previous call + if not self.token_returned: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "Token hasn't been returned." + "Token must be returned before can be returned only once.", + ) + + # Fail if token isn't matching + if request.token != clientapp_input.token: context.abort( grpc.StatusCode.INVALID_ARGUMENT, "Mismatch between ClientApp and SuperNode token", ) + + # Success + self.inputs_returned = True return PullClientAppInputsResponse( - message=message_to_proto(self.clientapp_input.message), - context=context_to_proto(self.clientapp_input.context), - run=run_to_proto(self.clientapp_input.run), + message=message_to_proto(clientapp_input.message), + context=context_to_proto(clientapp_input.context), + run=run_to_proto(clientapp_input.run), ) def PushClientAppOutputs( @@ -94,15 +143,39 @@ def PushClientAppOutputs( ) -> PushClientAppOutputsResponse: """Push Message and Context.""" log(DEBUG, "ClientAppIo.PushClientAppOutputs") - if self.clientapp_input is None: - raise ValueError( - "ClientAppIoInputs not set before calling `PushClientAppOutputs`." + + # Fail if no ClientAppIoInputs are available + if not self.clientapp_input: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "No inputs available.", + ) + clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + + # Fail if token wasn't returned in a previous call + if not self.token_returned: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "Token hasn't been returned." + "Token must be returned before can be returned only once.", ) - if request.token != self.clientapp_input.token: + + # Fail if inputs weren't delivered in a previous call + if not self.inputs_returned: + context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + "Inputs haven't been delivered." + "Inputs must be delivered before can be returned only once.", + ) + + # Fail if token isn't matching + if request.token != clientapp_input.token: context.abort( grpc.StatusCode.INVALID_ARGUMENT, "Mismatch between ClientApp and SuperNode token", ) + + # Preconditions met try: # Update Message and Context self.clientapp_output = ClientAppIoOutputs( @@ -113,32 +186,53 @@ def PushClientAppOutputs( # Set status code = typing.ClientAppOutputCode.SUCCESS status = typing.ClientAppOutputStatus(code=code, message="Success") - proto_status = clientappstatus_to_proto(status=status) - return PushClientAppOutputsResponse(status=proto_status) except Exception as e: # pylint: disable=broad-exception-caught log(ERROR, "ClientApp failed to push message to SuperNode, %s", e) code = typing.ClientAppOutputCode.UNKNOWN_ERROR - status = typing.ClientAppOutputStatus(code=code, message="Push failed") - proto_status = clientappstatus_to_proto(status=status) - return PushClientAppOutputsResponse(status=proto_status) - - def set_inputs(self, clientapp_input: ClientAppIoInputs) -> None: - """Set ClientApp inputs.""" + status = typing.ClientAppOutputStatus(code=code, message="Unkonwn error") + + # Return status to ClientApp process + proto_status = clientappstatus_to_proto(status=status) + return PushClientAppOutputsResponse(status=proto_status) + + def set_inputs( + self, clientapp_input: ClientAppIoInputs, token_returned: bool + ) -> None: + """Set ClientApp inputs. + + Parameters + ---------- + clientapp_input : ClientAppIoInputs + The inputs to the ClientApp. + token_returned : bool + A boolean indicating if the token has been returned. + Set to `True` when passing the token to `flwr-clientap` + and `False` otherwise. + """ log(DEBUG, "ClientAppIo.SetInputs") - if self.clientapp_input is not None or self.clientapp_output is not None: + if ( + self.clientapp_input is not None + or self.clientapp_output is not None + or self.token_returned + ): raise ValueError( "ClientAppIoInputs and ClientAppIoOutputs must not be set before " "calling `set_inputs`." ) self.clientapp_input = clientapp_input + self.token_returned = token_returned def get_outputs(self) -> ClientAppIoOutputs: """Get ClientApp outputs.""" log(DEBUG, "ClientAppIo.GetOutputs") if self.clientapp_output is None: raise ValueError("ClientAppIoOutputs not set before calling `get_outputs`.") - # Set outputs to a local variable and clear self.clientapp_output + + # Set outputs to a local variable and clear state output: ClientAppIoOutputs = self.clientapp_output self.clientapp_input = None self.clientapp_output = None + self.token_returned = False + self.inputs_returned = False + return output diff --git a/src/py/flwr/client/clientapp/clientappio_servicer_test.py b/src/py/flwr/client/clientapp/clientappio_servicer_test.py index f2efcef14e86..8011ae605195 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer_test.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer_test.py @@ -17,8 +17,9 @@ import unittest from unittest.mock import Mock, patch -from flwr.client.clientapp.app import pull_message, push_message +from flwr.client.clientapp.app import get_token, pull_message, push_message from flwr.common import Context, Message, typing +from flwr.common.constant import RUN_ID_NUM_BYTES from flwr.common.serde import ( clientappstatus_from_proto, clientappstatus_to_proto, @@ -28,11 +29,13 @@ # pylint:disable=E0611 from flwr.proto.clientappio_pb2 import ( + GetTokenResponse, PullClientAppInputsResponse, PushClientAppOutputsResponse, ) from flwr.proto.message_pb2 import Context as ProtoContext from flwr.proto.run_pb2 import Run as ProtoRun +from flwr.server.superlink.state.utils import generate_rand_int_from_bytes from .clientappio_servicer import ( ClientAppIoInputs, @@ -86,27 +89,27 @@ def test_set_inputs(self) -> None: with self.assertRaises(ValueError): self.servicer.clientapp_input = client_input self.servicer.clientapp_output = None - self.servicer.set_inputs(client_input) + self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert # - when ClientAppIoInputs is None, ClientAppIoOutputs is not None with self.assertRaises(ValueError): self.servicer.clientapp_input = None self.servicer.clientapp_output = client_output - self.servicer.set_inputs(client_input) + self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert # - when ClientAppIoInputs and ClientAppIoOutputs is not None with self.assertRaises(ValueError): self.servicer.clientapp_input = client_input self.servicer.clientapp_output = client_output - self.servicer.set_inputs(client_input) + self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert # - when ClientAppIoInputs is set at .clientapp_input self.servicer.clientapp_input = None self.servicer.clientapp_output = None - self.servicer.set_inputs(client_input) + self.servicer.set_inputs(client_input, token_returned=True) assert client_input == self.servicer.clientapp_input def test_get_outputs(self) -> None: @@ -194,3 +197,17 @@ def test_push_clientapp_outputs(self) -> None: # Assert self.mock_stub.PushClientAppOutputs.assert_called_once() self.assertEqual(status.message, "SUCCESS") + + def test_get_token(self) -> None: + """Test getting a token from SuperNode.""" + # Prepare + token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) + mock_response = GetTokenResponse(token=token) + self.mock_stub.GetToken.return_value = mock_response + + # Execute + res = get_token(stub=self.mock_stub) + + # Assert + self.mock_stub.GetToken.assert_called_once() + self.assertEqual(res, token) diff --git a/src/py/flwr/proto/clientappio_pb2.py b/src/py/flwr/proto/clientappio_pb2.py index 2234e3c2a8af..cb8435dfd82e 100644 --- a/src/py/flwr/proto/clientappio_pb2.py +++ b/src/py/flwr/proto/clientappio_pb2.py @@ -17,25 +17,29 @@ from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\"\x87\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xe4\x01\n\x0b\x43lientAppIo\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x11\n\x0fGetTokenRequest\"!\n\x10GetTokenResponse\x12\r\n\x05token\x18\x01 \x01(\x12\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\"\x87\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xad\x02\n\x0b\x43lientAppIo\x12G\n\x08GetToken\x12\x1b.flwr.proto.GetTokenRequest\x1a\x1c.flwr.proto.GetTokenResponse\"\x00\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.clientappio_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=591 - _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=667 + _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=645 + _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=721 _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_start=114 _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_end=201 - _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=203 - _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=246 - _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=249 - _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=384 - _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=386 - _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=506 - _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=508 - _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=589 - _globals['_CLIENTAPPIO']._serialized_start=670 - _globals['_CLIENTAPPIO']._serialized_end=898 + _globals['_GETTOKENREQUEST']._serialized_start=203 + _globals['_GETTOKENREQUEST']._serialized_end=220 + _globals['_GETTOKENRESPONSE']._serialized_start=222 + _globals['_GETTOKENRESPONSE']._serialized_end=255 + _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=257 + _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=300 + _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=303 + _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=438 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=440 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=560 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=562 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=643 + _globals['_CLIENTAPPIO']._serialized_start=724 + _globals['_CLIENTAPPIO']._serialized_end=1025 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/clientappio_pb2.pyi b/src/py/flwr/proto/clientappio_pb2.pyi index 31c9dc4c6d14..b3e57de28343 100644 --- a/src/py/flwr/proto/clientappio_pb2.pyi +++ b/src/py/flwr/proto/clientappio_pb2.pyi @@ -44,6 +44,23 @@ class ClientAppOutputStatus(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ... global___ClientAppOutputStatus = ClientAppOutputStatus +class GetTokenRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + def __init__(self, + ) -> None: ... +global___GetTokenRequest = GetTokenRequest + +class GetTokenResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TOKEN_FIELD_NUMBER: builtins.int + token: builtins.int + def __init__(self, + *, + token: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["token",b"token"]) -> None: ... +global___GetTokenResponse = GetTokenResponse + class PullClientAppInputsRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor TOKEN_FIELD_NUMBER: builtins.int diff --git a/src/py/flwr/proto/clientappio_pb2_grpc.py b/src/py/flwr/proto/clientappio_pb2_grpc.py index b244ef4a5b1d..653d49fc1ead 100644 --- a/src/py/flwr/proto/clientappio_pb2_grpc.py +++ b/src/py/flwr/proto/clientappio_pb2_grpc.py @@ -14,6 +14,11 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ + self.GetToken = channel.unary_unary( + '/flwr.proto.ClientAppIo/GetToken', + request_serializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.SerializeToString, + response_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.FromString, + ) self.PullClientAppInputs = channel.unary_unary( '/flwr.proto.ClientAppIo/PullClientAppInputs', request_serializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsRequest.SerializeToString, @@ -29,6 +34,13 @@ def __init__(self, channel): class ClientAppIoServicer(object): """Missing associated documentation comment in .proto file.""" + def GetToken(self, request, context): + """Get token + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def PullClientAppInputs(self, request, context): """Get Message, Context, and Run """ @@ -46,6 +58,11 @@ def PushClientAppOutputs(self, request, context): def add_ClientAppIoServicer_to_server(servicer, server): rpc_method_handlers = { + 'GetToken': grpc.unary_unary_rpc_method_handler( + servicer.GetToken, + request_deserializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.FromString, + response_serializer=flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.SerializeToString, + ), 'PullClientAppInputs': grpc.unary_unary_rpc_method_handler( servicer.PullClientAppInputs, request_deserializer=flwr_dot_proto_dot_clientappio__pb2.PullClientAppInputsRequest.FromString, @@ -66,6 +83,23 @@ def add_ClientAppIoServicer_to_server(servicer, server): class ClientAppIo(object): """Missing associated documentation comment in .proto file.""" + @staticmethod + def GetToken(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/GetToken', + flwr_dot_proto_dot_clientappio__pb2.GetTokenRequest.SerializeToString, + flwr_dot_proto_dot_clientappio__pb2.GetTokenResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def PullClientAppInputs(request, target, diff --git a/src/py/flwr/proto/clientappio_pb2_grpc.pyi b/src/py/flwr/proto/clientappio_pb2_grpc.pyi index 4503e11f17ae..3cddc769f745 100644 --- a/src/py/flwr/proto/clientappio_pb2_grpc.pyi +++ b/src/py/flwr/proto/clientappio_pb2_grpc.pyi @@ -8,6 +8,11 @@ import grpc class ClientAppIoStub: def __init__(self, channel: grpc.Channel) -> None: ... + GetToken: grpc.UnaryUnaryMultiCallable[ + flwr.proto.clientappio_pb2.GetTokenRequest, + flwr.proto.clientappio_pb2.GetTokenResponse] + """Get token""" + PullClientAppInputs: grpc.UnaryUnaryMultiCallable[ flwr.proto.clientappio_pb2.PullClientAppInputsRequest, flwr.proto.clientappio_pb2.PullClientAppInputsResponse] @@ -20,6 +25,14 @@ class ClientAppIoStub: class ClientAppIoServicer(metaclass=abc.ABCMeta): + @abc.abstractmethod + def GetToken(self, + request: flwr.proto.clientappio_pb2.GetTokenRequest, + context: grpc.ServicerContext, + ) -> flwr.proto.clientappio_pb2.GetTokenResponse: + """Get token""" + pass + @abc.abstractmethod def PullClientAppInputs(self, request: flwr.proto.clientappio_pb2.PullClientAppInputsRequest, From 66f3cb6083cfda270a545f388a6b105bc02eb5d4 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 18 Aug 2024 21:18:27 +0100 Subject: [PATCH 085/188] feat(framework) Use `GetToken` functionality (#4035) Co-authored-by: Daniel J. Beutel --- src/py/flwr/client/app.py | 10 ++++++++-- src/py/flwr/client/clientapp/app.py | 14 +++++++++++--- .../flwr/client/clientapp/clientappio_servicer.py | 6 ++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index fbade8bbfb22..65e1e1b4d365 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -21,7 +21,7 @@ from dataclasses import dataclass from logging import ERROR, INFO, WARN from pathlib import Path -from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union +from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union, cast import grpc from cryptography.hazmat.primitives.asymmetric import ec @@ -294,6 +294,7 @@ def _load_client_app(_1: str, _2: str) -> ClientApp: _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc( address=supernode_address ) + supernode_address = cast(str, supernode_address) # At this point, only `load_client_app_fn` should be used # Both `client` and `client_fn` must not be used directly @@ -454,7 +455,7 @@ def _on_backoff(retry_state: RetryState) -> None: # Handle app loading and task message try: - if isolate and supernode_address is not None: + if isolate: # Generate SuperNode token token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) @@ -483,6 +484,11 @@ def _on_backoff(retry_state: RetryState) -> None: stderr=None, check=True, ) + + # Wait for output to become available + while not clientappio_servicer.has_outputs(): + time.sleep(0.1) + outputs = clientappio_servicer.get_outputs() reply_message, context = outputs.message, outputs.context else: diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index 03a3a84af030..c2365e6ed680 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -58,10 +58,13 @@ def flwr_clientapp() -> None: ) parser.add_argument( "--supernode", + type=str, help="Address of SuperNode ClientAppIo gRPC servicer", ) parser.add_argument( "--token", + type=int, + required=False, help="Unique token generated by SuperNode for each ClientApp execution", ) args = parser.parse_args() @@ -72,7 +75,7 @@ def flwr_clientapp() -> None: args.supernode, args.token, ) - run_clientapp(supernode=args.supernode, token=int(args.token)) + run_clientapp(supernode=args.supernode, token=args.token) def on_channel_state_change(channel_connectivity: str) -> None: @@ -82,7 +85,7 @@ def on_channel_state_change(channel_connectivity: str) -> None: def run_clientapp( # pylint: disable=R0914 supernode: str, - token: int, + token: Optional[int] = None, ) -> None: """Run Flower ClientApp process. @@ -90,7 +93,7 @@ def run_clientapp( # pylint: disable=R0914 ---------- supernode : str Address of SuperNode - token : int + token : Optional[int] (default: None) Unique SuperNode token for ClientApp-SuperNode authentication """ channel = create_channel( @@ -102,6 +105,10 @@ def run_clientapp( # pylint: disable=R0914 try: stub = ClientAppIoStub(channel) + # If token is not set, loop until token is received from SuperNode + while token is None: + token = get_token(stub) + # Pull Message, Context, and Run from SuperNode message, context, run = pull_message(stub=stub, token=token) @@ -149,6 +156,7 @@ def run_clientapp( # pylint: disable=R0914 def get_token(stub: grpc.Channel) -> Optional[int]: """Get a token from SuperNode.""" + log(DEBUG, "Flower ClientApp process requests token") res: GetTokenResponse = stub.GetToken(GetTokenRequest()) return res.token diff --git a/src/py/flwr/client/clientapp/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py index 3483fe8fa8f7..0dc089591126 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer.py @@ -209,7 +209,6 @@ def set_inputs( Set to `True` when passing the token to `flwr-clientap` and `False` otherwise. """ - log(DEBUG, "ClientAppIo.SetInputs") if ( self.clientapp_input is not None or self.clientapp_output is not None @@ -222,9 +221,12 @@ def set_inputs( self.clientapp_input = clientapp_input self.token_returned = token_returned + def has_outputs(self) -> bool: + """Check if ClientAppOutputs are available.""" + return self.clientapp_output is not None + def get_outputs(self) -> ClientAppIoOutputs: """Get ClientApp outputs.""" - log(DEBUG, "ClientAppIo.GetOutputs") if self.clientapp_output is None: raise ValueError("ClientAppIoOutputs not set before calling `get_outputs`.") From 75b5de15def27b1ab297ce6cdd6463f7f37c5876 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 19 Aug 2024 12:23:18 +0200 Subject: [PATCH 086/188] feat(framework) Support different `--isolation` modes (#4037) --- src/py/flwr/client/app.py | 62 ++++++++++++++++++----------- src/py/flwr/client/supernode/app.py | 26 +++++++++--- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 65e1e1b4d365..6efb5f2e5018 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -62,6 +62,9 @@ ADDRESS_CLIENTAPPIO_API_GRPC_RERE = "0.0.0.0:9094" +ISOLATION_MODE_SUBPROCESS = "subprocess" +ISOLATION_MODE_PROCESS = "process" + def _check_actionable_client( client: Optional[Client], client_fn: Optional[ClientFnExt] @@ -207,7 +210,7 @@ def start_client_internal( max_retries: Optional[int] = None, max_wait_time: Optional[float] = None, flwr_path: Optional[Path] = None, - isolate: Optional[bool] = False, + isolation: Optional[str] = None, supernode_address: Optional[str] = ADDRESS_CLIENTAPPIO_API_GRPC_RERE, ) -> None: """Start a Flower client node which connects to a Flower server. @@ -256,11 +259,13 @@ class `flwr.client.Client` (default: None) If set to None, there is no limit to the total time. flwr_path: Optional[Path] (default: None) The fully resolved path containing installed Flower Apps. - isolate : Optional[bool] (default: False) - Whether to run `ClientApp` in a separate process. By default, this value is - `False`, and the `ClientApp` runs in the same process as the SuperNode. If - `True`, the `ClientApp` runs in an isolated process and communicates using - gRPC at the address `supernode_address`. + isolation : Optional[str] (default: None) + Isolation mode for `ClientApp`. Possible values are `subprocess` and + `process`. Defaults to `None`, which runs the `ClientApp` in the same process + as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started + by the SueprNode and communicates using gRPC at the address + `supernode_address`. If `process`, the `ClientApp` runs in a separate isolated + process and communicates using gRPC at the address `supernode_address`. supernode_address : Optional[str] (default: `ADDRESS_CLIENTAPPIO_API_GRPC_RERE`) The SuperNode gRPC server address. """ @@ -288,9 +293,12 @@ def _load_client_app(_1: str, _2: str) -> ClientApp: load_client_app_fn = _load_client_app - if isolate: + if isolation: if supernode_address is None: - raise ValueError("`supernode_address` required when `isolate` is set") + raise ValueError( + f"`supernode_address` required when `isolation` is " + f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}", + ) _clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc( address=supernode_address ) @@ -455,7 +463,14 @@ def _on_backoff(retry_state: RetryState) -> None: # Handle app loading and task message try: - if isolate: + if isolation: + # Two isolation modes: + # 1. `subprocess`: SuperNode is starting the ClientApp + # process as a subprocess. + # 2. `process`: ClientApp process gets started separately + # (via `flwr-clientapp`), for example, in a separate + # Docker container. + # Generate SuperNode token token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) @@ -470,20 +485,21 @@ def _on_backoff(retry_state: RetryState) -> None: token_returned=True, ) - # Run `ClientApp` in subprocess - command = [ - "flwr-clientapp", - "--supernode", - supernode_address, - "--token", - str(token), - ] - subprocess.run( - command, - stdout=None, - stderr=None, - check=True, - ) + if isolation == ISOLATION_MODE_SUBPROCESS: + # Start ClientApp subprocess + command = [ + "flwr-clientapp", + "--supernode", + supernode_address, + "--token", + str(token), + ] + subprocess.run( + command, + stdout=None, + stderr=None, + check=True, + ) # Wait for output to become available while not clientappio_servicer.has_outputs(): diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index ef17b9375d70..2e6b942b5d2f 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -37,7 +37,11 @@ from flwr.common.exit_handlers import register_exit_handlers from flwr.common.logger import log, warn_deprecated_feature -from ..app import start_client_internal +from ..app import ( + ISOLATION_MODE_PROCESS, + ISOLATION_MODE_SUBPROCESS, + start_client_internal, +) from ..clientapp.utils import get_load_client_app_fn ADDRESS_FLEET_API_GRPC_RERE = "0.0.0.0:9092" @@ -62,6 +66,8 @@ def run_supernode() -> None: ) authentication_keys = _try_setup_client_authentication(args) + log(DEBUG, "Isolation mode: %s", args.isolation) + start_client_internal( server_address=args.superlink, load_client_app_fn=load_fn, @@ -72,7 +78,7 @@ def run_supernode() -> None: max_retries=args.max_retries, max_wait_time=args.max_wait_time, node_config=parse_config_args([args.node_config]), - isolate=args.isolate, + isolation=args.isolation, supernode_address=args.supernode_address, ) @@ -199,10 +205,18 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser: """, ) parser.add_argument( - "--isolate", - action="store_true", - help="Run `ClientApp` in an isolated subprocess. By default, `ClientApp` " - "runs in the same process that executes the SuperNode.", + "--isolation", + default=None, + required=False, + choices=[ + ISOLATION_MODE_SUBPROCESS, + ISOLATION_MODE_PROCESS, + ], + help="Isolation mode when running `ClientApp` (optional, possible values: " + "`subprocess`, `process`). By default, `ClientApp` runs in the same process " + "that executes the SuperNode. Use `subprocess` to configure SuperNode to run " + "`ClientApp` in a subprocess. Use `process` to indicate that a separate " + "independent process gets created outside of SuperNode.", ) parser.add_argument( "--supernode-address", From 5c2aa61bfa353ea323273e5175780a0b2ec9140c Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 19 Aug 2024 13:17:04 +0100 Subject: [PATCH 087/188] fix(framework:skip) Avoid reinstalling `FAB` if already installed and confirmation is disabled (#4041) --- src/py/flwr/cli/install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/cli/install.py b/src/py/flwr/cli/install.py index 749a4516f65c..4318ccdf9ffb 100644 --- a/src/py/flwr/cli/install.py +++ b/src/py/flwr/cli/install.py @@ -173,7 +173,9 @@ def validate_and_install( / project_name / version ) - if install_dir.exists() and not skip_prompt: + if install_dir.exists(): + if skip_prompt: + return install_dir if not typer.confirm( typer.style( f"\nπŸ’¬ {project_name} version {version} is already installed, " From 35d87944e2bac83fdae811c21b64b498e2e92570 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 19 Aug 2024 14:35:37 +0200 Subject: [PATCH 088/188] feat(framework) Enable long-running `flwr-clientapp` (#4039) --- src/py/flwr/client/app.py | 17 ++- src/py/flwr/client/clientapp/app.py | 141 +++++++++++------- .../client/clientapp/clientappio_servicer.py | 1 + 3 files changed, 101 insertions(+), 58 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 6efb5f2e5018..8d2367e5c114 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -398,6 +398,7 @@ def _on_backoff(retry_state: RetryState) -> None: ) app_state_tracker.register_signal_handler() + # pylint: disable=too-many-nested-blocks while not app_state_tracker.interrupt: try: # Receive @@ -474,6 +475,9 @@ def _on_backoff(retry_state: RetryState) -> None: # Generate SuperNode token token: int = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) + # Mode 1: SuperNode starts ClientApp as subprocess + start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS + # Share Message and Context with servicer clientappio_servicer.set_inputs( clientapp_input=ClientAppIoInputs( @@ -482,10 +486,10 @@ def _on_backoff(retry_state: RetryState) -> None: run=run, token=token, ), - token_returned=True, + token_returned=start_subprocess, ) - if isolation == ISOLATION_MODE_SUBPROCESS: + if start_subprocess: # Start ClientApp subprocess command = [ "flwr-clientapp", @@ -500,10 +504,10 @@ def _on_backoff(retry_state: RetryState) -> None: stderr=None, check=True, ) - - # Wait for output to become available - while not clientappio_servicer.has_outputs(): - time.sleep(0.1) + else: + # Wait for output to become available + while not clientappio_servicer.has_outputs(): + time.sleep(0.1) outputs = clientappio_servicer.get_outputs() reply_message, context = outputs.message, outputs.context @@ -560,6 +564,7 @@ def _on_backoff(retry_state: RetryState) -> None: except StopIteration: sleep_duration = 0 break + # pylint: enable=too-many-nested-blocks # Unregister node if delete_node is not None and app_state_tracker.is_connected: diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index c2365e6ed680..09e3a0486ed2 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -15,6 +15,7 @@ """Flower ClientApp process.""" import argparse +import time from logging import DEBUG, ERROR, INFO from typing import Optional, Tuple @@ -68,10 +69,11 @@ def flwr_clientapp() -> None: help="Unique token generated by SuperNode for each ClientApp execution", ) args = parser.parse_args() + log( DEBUG, "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s " - "with the token %s", + "with token %s", args.supernode, args.token, ) @@ -105,46 +107,62 @@ def run_clientapp( # pylint: disable=R0914 try: stub = ClientAppIoStub(channel) - # If token is not set, loop until token is received from SuperNode - while token is None: - token = get_token(stub) - - # Pull Message, Context, and Run from SuperNode - message, context, run = pull_message(stub=stub, token=token) - - load_client_app_fn = get_load_client_app_fn( - default_app_ref="", - app_path=None, - multi_app=True, - flwr_dir=None, - ) - - try: - # Load ClientApp - client_app: ClientApp = load_client_app_fn(run.fab_id, run.fab_version) - - # Execute ClientApp - reply_message = client_app(message=message, context=context) - except Exception as ex: # pylint: disable=broad-exception-caught - # Don't update/change NodeState - - e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION - # Ex fmt: ":<'division by zero'>" - reason = str(type(ex)) + ":<'" + str(ex) + "'>" - exc_entity = "ClientApp" - if isinstance(ex, LoadClientAppError): - reason = "An exception was raised when attempting to load `ClientApp`" - e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION - - log(ERROR, "%s raised an exception", exc_entity, exc_info=ex) + only_once = token is not None + while True: + # If token is not set, loop until token is received from SuperNode + while token is None: + token = get_token(stub) + time.sleep(1) + + # Pull Message, Context, and Run from SuperNode + message, context, run = pull_message(stub=stub, token=token) + + load_client_app_fn = get_load_client_app_fn( + default_app_ref="", + app_path=None, + multi_app=True, + flwr_dir=None, + ) - # Create error message - reply_message = message.create_error_reply( - error=Error(code=e_code, reason=reason) + try: + # Load ClientApp + client_app: ClientApp = load_client_app_fn(run.fab_id, run.fab_version) + + # Execute ClientApp + reply_message = client_app(message=message, context=context) + except Exception as ex: # pylint: disable=broad-exception-caught + # Don't update/change NodeState + + e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION + # Ex fmt: ":<'division by zero'>" + reason = str(type(ex)) + ":<'" + str(ex) + "'>" + exc_entity = "ClientApp" + if isinstance(ex, LoadClientAppError): + reason = ( + "An exception was raised when attempting to load `ClientApp`" + ) + e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION + + log(ERROR, "%s raised an exception", exc_entity, exc_info=ex) + + # Create error message + reply_message = message.create_error_reply( + error=Error(code=e_code, reason=reason) + ) + + # Push Message and Context to SuperNode + _ = push_message( + stub=stub, token=token, message=reply_message, context=context ) - # Push Message and Context to SuperNode - _ = push_message(stub=stub, token=token, message=reply_message, context=context) + # Reset token to `None` to prevent flwr-clientapp from trying to pull the + # same inputs again + token = None + + # Stop the loop if `flwr-clientapp` is expected to process only a single + # message + if only_once: + break except KeyboardInterrupt: log(INFO, "Closing connection") @@ -157,30 +175,49 @@ def run_clientapp( # pylint: disable=R0914 def get_token(stub: grpc.Channel) -> Optional[int]: """Get a token from SuperNode.""" log(DEBUG, "Flower ClientApp process requests token") - res: GetTokenResponse = stub.GetToken(GetTokenRequest()) - return res.token + try: + res: GetTokenResponse = stub.GetToken(GetTokenRequest()) + log(DEBUG, "[GetToken] Received token: %s", res.token) + return res.token + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.FAILED_PRECONDITION: # pylint: disable=no-member + log(DEBUG, "[GetToken] No token available yet") + else: + log(ERROR, "[GetToken] gRPC error occurred: %s", str(e)) + return None def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run]: """Pull message from SuperNode to ClientApp.""" - res: PullClientAppInputsResponse = stub.PullClientAppInputs( - PullClientAppInputsRequest(token=token) - ) - message = message_from_proto(res.message) - context = context_from_proto(res.context) - run = run_from_proto(res.run) - return message, context, run + log(INFO, "Pulling ClientAppIoInputs for token %s", token) + try: + res: PullClientAppInputsResponse = stub.PullClientAppInputs( + PullClientAppInputsRequest(token=token) + ) + message = message_from_proto(res.message) + context = context_from_proto(res.context) + run = run_from_proto(res.run) + return message, context, run + except grpc.RpcError as e: + log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e)) + raise e def push_message( stub: grpc.Channel, token: int, message: Message, context: Context ) -> PushClientAppOutputsResponse: """Push message to SuperNode from ClientApp.""" + log(INFO, "Pushing ClientAppIoOutputs for token %s", token) proto_message = message_to_proto(message) proto_context = context_to_proto(context) - res: PushClientAppOutputsResponse = stub.PushClientAppOutputs( - PushClientAppOutputsRequest( - token=token, message=proto_message, context=proto_context + + try: + res: PushClientAppOutputsResponse = stub.PushClientAppOutputs( + PushClientAppOutputsRequest( + token=token, message=proto_message, context=proto_context + ) ) - ) - return res + return res + except grpc.RpcError as e: + log(ERROR, "[PushClientAppOutputs] gRPC error occurred: %s", str(e)) + raise e diff --git a/src/py/flwr/client/clientapp/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py index 0dc089591126..1f420c0a46e8 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer.py @@ -218,6 +218,7 @@ def set_inputs( "ClientAppIoInputs and ClientAppIoOutputs must not be set before " "calling `set_inputs`." ) + log(DEBUG, "ClientAppIoInputs set (token: %s)", clientapp_input.token) self.clientapp_input = clientapp_input self.token_returned = token_returned From f4f045fc07c7e4cbdca8688a5985e28025ad63d5 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 19 Aug 2024 13:56:32 +0100 Subject: [PATCH 089/188] refactor(framework:skip) Drop `Io` from `ClientApp{Input,Output}` dataclasses (#4042) --- src/py/flwr/client/app.py | 4 +- src/py/flwr/client/clientapp/app.py | 4 +- .../client/clientapp/clientappio_servicer.py | 38 +++++++++---------- .../clientapp/clientappio_servicer_test.py | 28 ++++++-------- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 8d2367e5c114..26ecd71211cf 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -52,7 +52,7 @@ from flwr.server.superlink.fleet.grpc_bidi.grpc_server import generic_create_grpc_server from flwr.server.superlink.state.utils import generate_rand_int_from_bytes -from .clientapp.clientappio_servicer import ClientAppIoInputs, ClientAppIoServicer +from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer from .grpc_adapter_client.connection import grpc_adapter from .grpc_client.connection import grpc_connection from .grpc_rere_client.connection import grpc_request_response @@ -480,7 +480,7 @@ def _on_backoff(retry_state: RetryState) -> None: # Share Message and Context with servicer clientappio_servicer.set_inputs( - clientapp_input=ClientAppIoInputs( + clientapp_input=ClientAppInputs( message=message, context=context, run=run, diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index 09e3a0486ed2..f7262d510e4d 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -189,7 +189,7 @@ def get_token(stub: grpc.Channel) -> Optional[int]: def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run]: """Pull message from SuperNode to ClientApp.""" - log(INFO, "Pulling ClientAppIoInputs for token %s", token) + log(INFO, "Pulling ClientAppInputs for token %s", token) try: res: PullClientAppInputsResponse = stub.PullClientAppInputs( PullClientAppInputsRequest(token=token) @@ -207,7 +207,7 @@ def push_message( stub: grpc.Channel, token: int, message: Message, context: Context ) -> PushClientAppOutputsResponse: """Push message to SuperNode from ClientApp.""" - log(INFO, "Pushing ClientAppIoOutputs for token %s", token) + log(INFO, "Pushing ClientAppOutputs for token %s", token) proto_message = message_to_proto(message) proto_context = context_to_proto(context) diff --git a/src/py/flwr/client/clientapp/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py index 1f420c0a46e8..020470ee1f33 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer.py @@ -46,7 +46,7 @@ @dataclass -class ClientAppIoInputs: +class ClientAppInputs: """Specify the inputs to the ClientApp.""" message: Message @@ -56,7 +56,7 @@ class ClientAppIoInputs: @dataclass -class ClientAppIoOutputs: +class ClientAppOutputs: """Specify the outputs from the ClientApp.""" message: Message @@ -68,8 +68,8 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer): """ClientAppIo API servicer.""" def __init__(self) -> None: - self.clientapp_input: Optional[ClientAppIoInputs] = None - self.clientapp_output: Optional[ClientAppIoOutputs] = None + self.clientapp_input: Optional[ClientAppInputs] = None + self.clientapp_output: Optional[ClientAppOutputs] = None self.token_returned: bool = False self.inputs_returned: bool = False @@ -79,13 +79,13 @@ def GetToken( """Get token.""" log(DEBUG, "ClientAppIo.GetToken") - # Fail if no ClientAppIoInputs are available + # Fail if no ClientAppInputs are available if self.clientapp_input is None: context.abort( grpc.StatusCode.FAILED_PRECONDITION, "No inputs available.", ) - clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + clientapp_input = cast(ClientAppInputs, self.clientapp_input) # Fail if token was already returned in a previous call if self.token_returned: @@ -95,7 +95,7 @@ def GetToken( ) # If - # - ClientAppIoInputs is set, and + # - ClientAppInputs is set, and # - token hasn't been returned before, # return token self.token_returned = True @@ -107,13 +107,13 @@ def PullClientAppInputs( """Pull Message, Context, and Run.""" log(DEBUG, "ClientAppIo.PullClientAppInputs") - # Fail if no ClientAppIoInputs are available + # Fail if no ClientAppInputs are available if self.clientapp_input is None: context.abort( grpc.StatusCode.FAILED_PRECONDITION, "No inputs available.", ) - clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + clientapp_input = cast(ClientAppInputs, self.clientapp_input) # Fail if token wasn't returned in a previous call if not self.token_returned: @@ -144,13 +144,13 @@ def PushClientAppOutputs( """Push Message and Context.""" log(DEBUG, "ClientAppIo.PushClientAppOutputs") - # Fail if no ClientAppIoInputs are available + # Fail if no ClientAppInputs are available if not self.clientapp_input: context.abort( grpc.StatusCode.FAILED_PRECONDITION, "No inputs available.", ) - clientapp_input = cast(ClientAppIoInputs, self.clientapp_input) + clientapp_input = cast(ClientAppInputs, self.clientapp_input) # Fail if token wasn't returned in a previous call if not self.token_returned: @@ -178,7 +178,7 @@ def PushClientAppOutputs( # Preconditions met try: # Update Message and Context - self.clientapp_output = ClientAppIoOutputs( + self.clientapp_output = ClientAppOutputs( message=message_from_proto(request.message), context=context_from_proto(request.context), ) @@ -196,13 +196,13 @@ def PushClientAppOutputs( return PushClientAppOutputsResponse(status=proto_status) def set_inputs( - self, clientapp_input: ClientAppIoInputs, token_returned: bool + self, clientapp_input: ClientAppInputs, token_returned: bool ) -> None: """Set ClientApp inputs. Parameters ---------- - clientapp_input : ClientAppIoInputs + clientapp_input : ClientAppInputs The inputs to the ClientApp. token_returned : bool A boolean indicating if the token has been returned. @@ -215,10 +215,10 @@ def set_inputs( or self.token_returned ): raise ValueError( - "ClientAppIoInputs and ClientAppIoOutputs must not be set before " + "ClientAppInputs and ClientAppOutputs must not be set before " "calling `set_inputs`." ) - log(DEBUG, "ClientAppIoInputs set (token: %s)", clientapp_input.token) + log(DEBUG, "ClientAppInputs set (token: %s)", clientapp_input.token) self.clientapp_input = clientapp_input self.token_returned = token_returned @@ -226,13 +226,13 @@ def has_outputs(self) -> bool: """Check if ClientAppOutputs are available.""" return self.clientapp_output is not None - def get_outputs(self) -> ClientAppIoOutputs: + def get_outputs(self) -> ClientAppOutputs: """Get ClientApp outputs.""" if self.clientapp_output is None: - raise ValueError("ClientAppIoOutputs not set before calling `get_outputs`.") + raise ValueError("ClientAppOutputs not set before calling `get_outputs`.") # Set outputs to a local variable and clear state - output: ClientAppIoOutputs = self.clientapp_output + output: ClientAppOutputs = self.clientapp_output self.clientapp_input = None self.clientapp_output = None self.token_returned = False diff --git a/src/py/flwr/client/clientapp/clientappio_servicer_test.py b/src/py/flwr/client/clientapp/clientappio_servicer_test.py index 8011ae605195..cdffc698ef3b 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer_test.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer_test.py @@ -37,11 +37,7 @@ from flwr.proto.run_pb2 import Run as ProtoRun from flwr.server.superlink.state.utils import generate_rand_int_from_bytes -from .clientappio_servicer import ( - ClientAppIoInputs, - ClientAppIoOutputs, - ClientAppIoServicer, -) +from .clientappio_servicer import ClientAppInputs, ClientAppIoServicer, ClientAppOutputs class TestClientAppIoServicer(unittest.TestCase): @@ -81,32 +77,32 @@ def test_set_inputs(self) -> None: fab_hash="dolor", override_config=self.maker.user_config(), ) - client_input = ClientAppIoInputs(message, context, run, 1) - client_output = ClientAppIoOutputs(message, context) + client_input = ClientAppInputs(message, context, run, 1) + client_output = ClientAppOutputs(message, context) # Execute and assert - # - when ClientAppIoInputs is not None, ClientAppIoOutputs is None + # - when ClientAppInputs is not None, ClientAppOutputs is None with self.assertRaises(ValueError): self.servicer.clientapp_input = client_input self.servicer.clientapp_output = None self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert - # - when ClientAppIoInputs is None, ClientAppIoOutputs is not None + # - when ClientAppInputs is None, ClientAppOutputs is not None with self.assertRaises(ValueError): self.servicer.clientapp_input = None self.servicer.clientapp_output = client_output self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert - # - when ClientAppIoInputs and ClientAppIoOutputs is not None + # - when ClientAppInputs and ClientAppOutputs is not None with self.assertRaises(ValueError): self.servicer.clientapp_input = client_input self.servicer.clientapp_output = client_output self.servicer.set_inputs(client_input, token_returned=True) # Execute and assert - # - when ClientAppIoInputs is set at .clientapp_input + # - when ClientAppInputs is set at .clientapp_input self.servicer.clientapp_input = None self.servicer.clientapp_output = None self.servicer.set_inputs(client_input, token_returned=True) @@ -125,18 +121,18 @@ def test_get_outputs(self) -> None: state=self.maker.recordset(2, 2, 1), run_config={"runconfig1": 6.1}, ) - client_output = ClientAppIoOutputs(message, context) + client_output = ClientAppOutputs(message, context) - # Execute and assert - when `ClientAppIoOutputs` is None + # Execute and assert - when `ClientAppOutputs` is None self.servicer.clientapp_output = None with self.assertRaises(ValueError): - # `ClientAppIoOutputs` should not be None + # `ClientAppOutputs` should not be None _ = self.servicer.get_outputs() - # Execute and assert - when `ClientAppIoOutputs` is not None + # Execute and assert - when `ClientAppOutputs` is not None self.servicer.clientapp_output = client_output output = self.servicer.get_outputs() - assert isinstance(output, ClientAppIoOutputs) + assert isinstance(output, ClientAppOutputs) assert output == client_output assert self.servicer.clientapp_input is None assert self.servicer.clientapp_output is None From 49027de9d6f1870856952ca9430b6dfc910f3670 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:15:23 +0200 Subject: [PATCH 090/188] build(deps): bump actions/upload-artifact from 4.3.4 to 4.3.6 (#4038) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.4 to 4.3.6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b2256b8c012f0828dc542b3febcab082c67f72b...834a144ee995460fba8ed112a2fc961b36a5ec5a) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Robert Steiner --- .github/workflows/_docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index e1faa116f3f3..a3373c6e93fa 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -122,7 +122,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: digests-${{ steps.build-id.outputs.id }}-${{ matrix.platform.name }} path: /tmp/digests/* From 221dd318a40aa649bce01aaf68e347c6f7f59010 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 19 Aug 2024 21:18:27 +0100 Subject: [PATCH 091/188] feat(framework) Deliver `FAB` to `ClientApp` process (#4036) --- src/proto/flwr/proto/clientappio.proto | 1 + src/py/flwr/client/app.py | 1 + src/py/flwr/client/clientapp/app.py | 12 +++++++---- .../client/clientapp/clientappio_servicer.py | 5 ++++- .../clientapp/clientappio_servicer_test.py | 18 +++++++++++++++-- src/py/flwr/proto/clientappio_pb2.py | 20 +++++++++---------- src/py/flwr/proto/clientappio_pb2.pyi | 9 +++++++-- 7 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/proto/flwr/proto/clientappio.proto b/src/proto/flwr/proto/clientappio.proto index f4f52184634e..898cb04c5b5b 100644 --- a/src/proto/flwr/proto/clientappio.proto +++ b/src/proto/flwr/proto/clientappio.proto @@ -37,6 +37,7 @@ message PullClientAppInputsResponse { Message message = 1; Context context = 2; Run run = 3; + Fab fab = 4; } message PushClientAppOutputsRequest { diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 26ecd71211cf..7b2a9d16b883 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -484,6 +484,7 @@ def _on_backoff(retry_state: RetryState) -> None: message=message, context=context, run=run, + fab=fab, token=token, ), token_returned=start_subprocess, diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index f7262d510e4d..83e3f75a31b1 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -30,11 +30,12 @@ from flwr.common.serde import ( context_from_proto, context_to_proto, + fab_from_proto, message_from_proto, message_to_proto, run_from_proto, ) -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run # pylint: disable=E0611 from flwr.proto.clientappio_pb2 import ( @@ -115,7 +116,7 @@ def run_clientapp( # pylint: disable=R0914 time.sleep(1) # Pull Message, Context, and Run from SuperNode - message, context, run = pull_message(stub=stub, token=token) + message, context, run, _ = pull_message(stub=stub, token=token) load_client_app_fn = get_load_client_app_fn( default_app_ref="", @@ -187,7 +188,9 @@ def get_token(stub: grpc.Channel) -> Optional[int]: return None -def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run]: +def pull_message( + stub: grpc.Channel, token: int +) -> Tuple[Message, Context, Run, Optional[Fab]]: """Pull message from SuperNode to ClientApp.""" log(INFO, "Pulling ClientAppInputs for token %s", token) try: @@ -197,7 +200,8 @@ def pull_message(stub: grpc.Channel, token: int) -> Tuple[Message, Context, Run] message = message_from_proto(res.message) context = context_from_proto(res.context) run = run_from_proto(res.run) - return message, context, run + fab = fab_from_proto(res.fab) if res.fab else None + return message, context, run, fab except grpc.RpcError as e: log(ERROR, "[PullClientAppInputs] gRPC error occurred: %s", str(e)) raise e diff --git a/src/py/flwr/client/clientapp/clientappio_servicer.py b/src/py/flwr/client/clientapp/clientappio_servicer.py index 020470ee1f33..fe7ccd6e908f 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer.py @@ -27,11 +27,12 @@ clientappstatus_to_proto, context_from_proto, context_to_proto, + fab_to_proto, message_from_proto, message_to_proto, run_to_proto, ) -from flwr.common.typing import Run +from flwr.common.typing import Fab, Run # pylint: disable=E0611 from flwr.proto import clientappio_pb2_grpc @@ -52,6 +53,7 @@ class ClientAppInputs: message: Message context: Context run: Run + fab: Optional[Fab] token: int @@ -136,6 +138,7 @@ def PullClientAppInputs( message=message_to_proto(clientapp_input.message), context=context_to_proto(clientapp_input.context), run=run_to_proto(clientapp_input.run), + fab=fab_to_proto(clientapp_input.fab) if clientapp_input.fab else None, ) def PushClientAppOutputs( diff --git a/src/py/flwr/client/clientapp/clientappio_servicer_test.py b/src/py/flwr/client/clientapp/clientappio_servicer_test.py index cdffc698ef3b..a03400c12a86 100644 --- a/src/py/flwr/client/clientapp/clientappio_servicer_test.py +++ b/src/py/flwr/client/clientapp/clientappio_servicer_test.py @@ -23,6 +23,7 @@ from flwr.common.serde import ( clientappstatus_from_proto, clientappstatus_to_proto, + fab_to_proto, message_to_proto, ) from flwr.common.serde_test import RecordMaker @@ -77,7 +78,12 @@ def test_set_inputs(self) -> None: fab_hash="dolor", override_config=self.maker.user_config(), ) - client_input = ClientAppInputs(message, context, run, 1) + fab = typing.Fab( + hash_str="abc123#$%", + content=b"\xf3\xf5\xf8\x98", + ) + + client_input = ClientAppInputs(message, context, run, fab, 1) client_output = ClientAppOutputs(message, context) # Execute and assert @@ -144,15 +150,20 @@ def test_pull_clientapp_inputs(self) -> None: metadata=self.maker.metadata(), content=self.maker.recordset(3, 2, 1), ) + mock_fab = typing.Fab( + hash_str="abc123#$%", + content=b"\xf3\xf5\xf8\x98", + ) mock_response = PullClientAppInputsResponse( message=message_to_proto(mock_message), context=ProtoContext(node_id=123), run=ProtoRun(run_id=61016, fab_id="mock/mock", fab_version="v1.0.0"), + fab=fab_to_proto(mock_fab), ) self.mock_stub.PullClientAppInputs.return_value = mock_response # Execute - message, context, run = pull_message(self.mock_stub, token=456) + message, context, run, fab = pull_message(self.mock_stub, token=456) # Assert self.mock_stub.PullClientAppInputs.assert_called_once() @@ -163,6 +174,9 @@ def test_pull_clientapp_inputs(self) -> None: self.assertEqual(run.run_id, 61016) self.assertEqual(run.fab_id, "mock/mock") self.assertEqual(run.fab_version, "v1.0.0") + if fab: + self.assertEqual(fab.hash_str, mock_fab.hash_str) + self.assertEqual(fab.content, mock_fab.content) def test_push_clientapp_outputs(self) -> None: """Test pushing messages to SuperNode.""" diff --git a/src/py/flwr/proto/clientappio_pb2.py b/src/py/flwr/proto/clientappio_pb2.py index cb8435dfd82e..9fd5302fe6cd 100644 --- a/src/py/flwr/proto/clientappio_pb2.py +++ b/src/py/flwr/proto/clientappio_pb2.py @@ -17,15 +17,15 @@ from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x11\n\x0fGetTokenRequest\"!\n\x10GetTokenResponse\x12\r\n\x05token\x18\x01 \x01(\x12\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\"\x87\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xad\x02\n\x0b\x43lientAppIo\x12G\n\x08GetToken\x12\x1b.flwr.proto.GetTokenRequest\x1a\x1c.flwr.proto.GetTokenResponse\"\x00\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\"W\n\x15\x43lientAppOutputStatus\x12-\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x1f.flwr.proto.ClientAppOutputCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x11\n\x0fGetTokenRequest\"!\n\x10GetTokenResponse\x12\r\n\x05token\x18\x01 \x01(\x12\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\"\xa5\x01\n\x1bPullClientAppInputsResponse\x12$\n\x07message\x18\x01 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Context\x12\x1c\n\x03run\x18\x03 \x01(\x0b\x32\x0f.flwr.proto.Run\x12\x1c\n\x03\x66\x61\x62\x18\x04 \x01(\x0b\x32\x0f.flwr.proto.Fab\"x\n\x1bPushClientAppOutputsRequest\x12\r\n\x05token\x18\x01 \x01(\x12\x12$\n\x07message\x18\x02 \x01(\x0b\x32\x13.flwr.proto.Message\x12$\n\x07\x63ontext\x18\x03 \x01(\x0b\x32\x13.flwr.proto.Context\"Q\n\x1cPushClientAppOutputsResponse\x12\x31\n\x06status\x18\x01 \x01(\x0b\x32!.flwr.proto.ClientAppOutputStatus*L\n\x13\x43lientAppOutputCode\x12\x0b\n\x07SUCCESS\x10\x00\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x01\x12\x11\n\rUNKNOWN_ERROR\x10\x02\x32\xad\x02\n\x0b\x43lientAppIo\x12G\n\x08GetToken\x12\x1b.flwr.proto.GetTokenRequest\x1a\x1c.flwr.proto.GetTokenResponse\"\x00\x12h\n\x13PullClientAppInputs\x12&.flwr.proto.PullClientAppInputsRequest\x1a\'.flwr.proto.PullClientAppInputsResponse\"\x00\x12k\n\x14PushClientAppOutputs\x12\'.flwr.proto.PushClientAppOutputsRequest\x1a(.flwr.proto.PushClientAppOutputsResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'flwr.proto.clientappio_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=645 - _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=721 + _globals['_CLIENTAPPOUTPUTCODE']._serialized_start=675 + _globals['_CLIENTAPPOUTPUTCODE']._serialized_end=751 _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_start=114 _globals['_CLIENTAPPOUTPUTSTATUS']._serialized_end=201 _globals['_GETTOKENREQUEST']._serialized_start=203 @@ -35,11 +35,11 @@ _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_start=257 _globals['_PULLCLIENTAPPINPUTSREQUEST']._serialized_end=300 _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_start=303 - _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=438 - _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=440 - _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=560 - _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=562 - _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=643 - _globals['_CLIENTAPPIO']._serialized_start=724 - _globals['_CLIENTAPPIO']._serialized_end=1025 + _globals['_PULLCLIENTAPPINPUTSRESPONSE']._serialized_end=468 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_start=470 + _globals['_PUSHCLIENTAPPOUTPUTSREQUEST']._serialized_end=590 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_start=592 + _globals['_PUSHCLIENTAPPOUTPUTSRESPONSE']._serialized_end=673 + _globals['_CLIENTAPPIO']._serialized_start=754 + _globals['_CLIENTAPPIO']._serialized_end=1055 # @@protoc_insertion_point(module_scope) diff --git a/src/py/flwr/proto/clientappio_pb2.pyi b/src/py/flwr/proto/clientappio_pb2.pyi index b3e57de28343..53d376d58101 100644 --- a/src/py/flwr/proto/clientappio_pb2.pyi +++ b/src/py/flwr/proto/clientappio_pb2.pyi @@ -3,6 +3,7 @@ isort:skip_file """ import builtins +import flwr.proto.fab_pb2 import flwr.proto.message_pb2 import flwr.proto.run_pb2 import google.protobuf.descriptor @@ -77,20 +78,24 @@ class PullClientAppInputsResponse(google.protobuf.message.Message): MESSAGE_FIELD_NUMBER: builtins.int CONTEXT_FIELD_NUMBER: builtins.int RUN_FIELD_NUMBER: builtins.int + FAB_FIELD_NUMBER: builtins.int @property def message(self) -> flwr.proto.message_pb2.Message: ... @property def context(self) -> flwr.proto.message_pb2.Context: ... @property def run(self) -> flwr.proto.run_pb2.Run: ... + @property + def fab(self) -> flwr.proto.fab_pb2.Fab: ... def __init__(self, *, message: typing.Optional[flwr.proto.message_pb2.Message] = ..., context: typing.Optional[flwr.proto.message_pb2.Context] = ..., run: typing.Optional[flwr.proto.run_pb2.Run] = ..., + fab: typing.Optional[flwr.proto.fab_pb2.Fab] = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message","run",b"run"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["context",b"context","message",b"message","run",b"run"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["context",b"context","fab",b"fab","message",b"message","run",b"run"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["context",b"context","fab",b"fab","message",b"message","run",b"run"]) -> None: ... global___PullClientAppInputsResponse = PullClientAppInputsResponse class PushClientAppOutputsRequest(google.protobuf.message.Message): From a0f9dd5419e6a34574bc911347bee294145be696 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 19 Aug 2024 21:43:07 +0100 Subject: [PATCH 092/188] feat(framework) Enable installation of `FAB` in `ClientApp` process (#4044) --- src/py/flwr/client/app.py | 4 +++- src/py/flwr/client/clientapp/app.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 7b2a9d16b883..1aed5d5241dd 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -440,7 +440,9 @@ def _on_backoff(retry_state: RetryState) -> None: run: Run = runs[run_id] if get_fab is not None and run.fab_hash: fab = get_fab(run.fab_hash) - install_from_fab(fab.content, flwr_path, True) + if not isolation: + # If `ClientApp` runs in the same process, install the FAB + install_from_fab(fab.content, flwr_path, True) fab_id, fab_version = get_fab_metadata(fab.content) else: fab = None diff --git a/src/py/flwr/client/clientapp/app.py b/src/py/flwr/client/clientapp/app.py index 83e3f75a31b1..69d334fead14 100644 --- a/src/py/flwr/client/clientapp/app.py +++ b/src/py/flwr/client/clientapp/app.py @@ -21,6 +21,7 @@ import grpc +from flwr.cli.install import install_from_fab from flwr.client.client_app import ClientApp, LoadClientAppError from flwr.common import Context, Message from flwr.common.constant import ErrorCode @@ -115,8 +116,13 @@ def run_clientapp( # pylint: disable=R0914 token = get_token(stub) time.sleep(1) - # Pull Message, Context, and Run from SuperNode - message, context, run, _ = pull_message(stub=stub, token=token) + # Pull Message, Context, Run and (optional) FAB from SuperNode + message, context, run, fab = pull_message(stub=stub, token=token) + + # Install FAB, if provided + if fab: + log(DEBUG, "Flower ClientApp starts FAB installation.") + install_from_fab(fab.content, flwr_dir=None, skip_prompt=True) load_client_app_fn = get_load_client_app_fn( default_app_ref="", From 7550a1177c962ccafc5768b39a8918fd46aa77a8 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Mon, 19 Aug 2024 22:59:51 +0200 Subject: [PATCH 093/188] fix(framework) Add log to differentiate authenticated create node (#4043) Co-authored-by: Daniel J. Beutel --- .../server/superlink/fleet/grpc_rere/fleet_servicer.py | 4 +++- .../superlink/fleet/grpc_rere/server_interceptor.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py index c14bca93a127..e0501e54fafc 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py @@ -52,10 +52,12 @@ def CreateNode( ) -> CreateNodeResponse: """.""" log(INFO, "FleetServicer.CreateNode") - return message_handler.create_node( + response = message_handler.create_node( request=request, state=self.state_factory.state(), ) + log(INFO, "FleetServicer: Created node_id=%s", response.node.node_id) + return response def DeleteNode( self, request: DeleteNodeRequest, context: grpc.ServicerContext diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py index 21e9c44907cd..87ac45a4f9c8 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py @@ -16,7 +16,7 @@ import base64 -from logging import WARNING +from logging import INFO, WARNING from typing import Any, Callable, Optional, Sequence, Tuple, Union import grpc @@ -128,9 +128,15 @@ def _generic_method_handler( context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied") if isinstance(request, CreateNodeRequest): - return self._create_authenticated_node( + response = self._create_authenticated_node( client_public_key_bytes, request, context ) + log( + INFO, + "AuthenticateServerInterceptor: Created node_id=%s", + response.node.node_id, + ) + return response # Verify hmac value hmac_value = base64.urlsafe_b64decode( From 88a88aa483351266b04e5d81c2d777428d052a2d Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Mon, 19 Aug 2024 23:09:22 +0200 Subject: [PATCH 094/188] docs(framework) Adjust contributor image (#4040) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f61d616775b..1dd686e5f1b6 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Other [examples](https://github.com/adap/flower/tree/main/examples): Flower is built by a wonderful community of researchers and engineers. [Join Slack](https://flower.ai/join-slack) to meet them, [contributions](#contributing-to-flower) are welcome. - + ## Citation From 52f9437a6d14dda8802a4497a7cc8d364a4aee24 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Tue, 20 Aug 2024 11:41:16 +0200 Subject: [PATCH 095/188] feat(framework) Add ClientApp Docker image (#3998) Signed-off-by: Robert Steiner --- .github/workflows/release-nightly.yml | 3 ++- dev/build-docker-image-matrix.py | 7 +++++++ src/docker/clientapp/Dockerfile | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/docker/clientapp/Dockerfile diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 97751aafc031..fcefff300cb7 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -70,7 +70,8 @@ jobs: { repository: "flwr/superlink", file_dir: "src/docker/superlink" }, { repository: "flwr/supernode", file_dir: "src/docker/supernode" }, { repository: "flwr/serverapp", file_dir: "src/docker/serverapp" }, - { repository: "flwr/superexec", file_dir: "src/docker/superexec" } + { repository: "flwr/superexec", file_dir: "src/docker/superexec" }, + { repository: "flwr/clientapp", file_dir: "src/docker/clientapp" } ] with: namespace-repository: ${{ matrix.images.repository }} diff --git a/dev/build-docker-image-matrix.py b/dev/build-docker-image-matrix.py index f10ae03245d0..bcee5ef1c90e 100644 --- a/dev/build-docker-image-matrix.py +++ b/dev/build-docker-image-matrix.py @@ -175,6 +175,13 @@ def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: tag_latest_ubuntu_with_flwr_version, lambda image: image.distro.name == DistroName.UBUNTU, ) + # ubuntu images for each supported python version + + generate_binary_images( + "clientapp", + base_images, + tag_latest_ubuntu_with_flwr_version, + lambda image: image.distro.name == DistroName.UBUNTU, + ) ) print( diff --git a/src/docker/clientapp/Dockerfile b/src/docker/clientapp/Dockerfile new file mode 100644 index 000000000000..0f5e2b1d81a1 --- /dev/null +++ b/src/docker/clientapp/Dockerfile @@ -0,0 +1,20 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +ARG BASE_REPOSITORY=flwr/base +ARG BASE_IMAGE +FROM $BASE_REPOSITORY:$BASE_IMAGE + +ENTRYPOINT ["flwr-clientapp"] From 9b5858636968de4504e585111394aee83982b0dc Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Tue, 20 Aug 2024 12:38:43 +0200 Subject: [PATCH 096/188] refactor(*:skip) Fix client interceptor tests (#4047) --- .../client_interceptor_test.py | 183 +++++++++++------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py index f5d3a6d2b6f9..79416a8eb31b 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py @@ -25,7 +25,7 @@ import grpc from flwr.client.grpc_rere_client.connection import grpc_request_response -from flwr.common import GRPC_MAX_MESSAGE_LENGTH +from flwr.common import GRPC_MAX_MESSAGE_LENGTH, serde from flwr.common.logger import log from flwr.common.message import Message, Metadata from flwr.common.record import RecordSet @@ -46,7 +46,9 @@ PushTaskResRequest, PushTaskResResponse, ) +from flwr.proto.node_pb2 import Node # pylint: disable=E0611 from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 +from flwr.proto.task_pb2 import Task, TaskIns # pylint: disable=E0611 from .client_interceptor import _AUTH_TOKEN_HEADER, _PUBLIC_KEY_HEADER, Request @@ -75,15 +77,31 @@ def unary_unary( if isinstance(request, CreateNodeRequest): context.send_initial_metadata( - ((_PUBLIC_KEY_HEADER, self.server_public_key),) + ( + ( + _PUBLIC_KEY_HEADER, + base64.urlsafe_b64encode( + public_key_to_bytes(self.server_public_key) + ), + ), + ) ) - return CreateNodeResponse() + return CreateNodeResponse(node=Node(node_id=123)) if isinstance(request, DeleteNodeRequest): return DeleteNodeResponse() if isinstance(request, PushTaskResRequest): return PushTaskResResponse() - return PullTaskInsResponse() + return PullTaskInsResponse( + task_ins_list=[ + TaskIns( + task=Task( + consumer=Node(node_id=123), + recordset=serde.recordset_to_proto(RecordSet()), + ) + ) + ] + ) def received_client_metadata( self, @@ -132,6 +150,16 @@ def _add_generic_handler(servicer: _MockServicer, server: grpc.Server) -> None: server.add_generic_rpc_handlers((generic_handler,)) +def _get_value_from_tuples( + key_string: str, tuples: Sequence[Tuple[str, Union[str, bytes]]] +) -> bytes: + value = next((value for key, value in tuples if key == key_string), "") + if isinstance(value, str): + return value.encode() + + return value + + def _init_retry_invoker() -> RetryInvoker: return RetryInvoker( wait_gen_factory=exponential, @@ -205,13 +233,20 @@ def test_client_auth_create_node(self) -> None: _, _, create_node, _, _, _ = conn assert create_node is not None create_node() - expected_client_metadata = ( - _PUBLIC_KEY_HEADER, - base64.urlsafe_b64encode(public_key_to_bytes(self._client_public_key)), + + received_metadata = self._servicer.received_client_metadata() + assert received_metadata is not None + + actual_public_key = _get_value_from_tuples( + _PUBLIC_KEY_HEADER, received_metadata + ) + + expected_public_key = base64.urlsafe_b64encode( + public_key_to_bytes(self._client_public_key) ) # Assert - assert self._servicer.received_client_metadata() == expected_client_metadata + assert actual_public_key == expected_public_key def test_client_auth_delete_node(self) -> None: """Test client authentication during delete node.""" @@ -227,30 +262,32 @@ def test_client_auth_delete_node(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, _, _, delete_node, _, _ = conn + _, _, create_node, delete_node, _, _ = conn + assert create_node is not None + create_node() assert delete_node is not None delete_node() + + received_metadata = self._servicer.received_client_metadata() + assert received_metadata is not None + shared_secret = generate_shared_key( self._servicer.server_private_key, self._client_public_key ) - expected_hmac = compute_hmac( - shared_secret, self._servicer.received_message_bytes() + expected_hmac = base64.urlsafe_b64encode( + compute_hmac(shared_secret, self._servicer.received_message_bytes()) ) - expected_client_metadata = ( - ( - _PUBLIC_KEY_HEADER, - base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) - ), - ), - ( - _AUTH_TOKEN_HEADER, - base64.urlsafe_b64encode(expected_hmac), - ), + actual_public_key = _get_value_from_tuples( + _PUBLIC_KEY_HEADER, received_metadata + ) + actual_hmac = _get_value_from_tuples(_AUTH_TOKEN_HEADER, received_metadata) + expected_public_key = base64.urlsafe_b64encode( + public_key_to_bytes(self._client_public_key) ) # Assert - assert self._servicer.received_client_metadata() == expected_client_metadata + assert actual_public_key == expected_public_key + assert actual_hmac == expected_hmac def test_client_auth_receive(self) -> None: """Test client authentication during receive node.""" @@ -266,36 +303,38 @@ def test_client_auth_receive(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - receive, _, _, _, _, _ = conn + receive, _, create_node, _, _, _ = conn + assert create_node is not None + create_node() assert receive is not None receive() + + received_metadata = self._servicer.received_client_metadata() + assert received_metadata is not None + shared_secret = generate_shared_key( self._servicer.server_private_key, self._client_public_key ) - expected_hmac = compute_hmac( - shared_secret, self._servicer.received_message_bytes() + expected_hmac = base64.urlsafe_b64encode( + compute_hmac(shared_secret, self._servicer.received_message_bytes()) ) - expected_client_metadata = ( - ( - _PUBLIC_KEY_HEADER, - base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) - ), - ), - ( - _AUTH_TOKEN_HEADER, - base64.urlsafe_b64encode(expected_hmac), - ), + actual_public_key = _get_value_from_tuples( + _PUBLIC_KEY_HEADER, received_metadata + ) + actual_hmac = _get_value_from_tuples(_AUTH_TOKEN_HEADER, received_metadata) + expected_public_key = base64.urlsafe_b64encode( + public_key_to_bytes(self._client_public_key) ) # Assert - assert self._servicer.received_client_metadata() == expected_client_metadata + assert actual_public_key == expected_public_key + assert actual_hmac == expected_hmac def test_client_auth_send(self) -> None: """Test client authentication during send node.""" # Prepare retry_invoker = _init_retry_invoker() - message = Message(Metadata(0, "1", 0, 0, "", "", 0, ""), RecordSet()) + message = Message(Metadata(0, "", 123, 0, "", "", 0, ""), RecordSet()) # Execute with self._connection( @@ -306,30 +345,34 @@ def test_client_auth_send(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, send, _, _, _, _ = conn + receive, send, create_node, _, _, _ = conn + assert create_node is not None + create_node() + assert receive is not None + receive() assert send is not None send(message) + + received_metadata = self._servicer.received_client_metadata() + assert received_metadata is not None + shared_secret = generate_shared_key( self._servicer.server_private_key, self._client_public_key ) - expected_hmac = compute_hmac( - shared_secret, self._servicer.received_message_bytes() + expected_hmac = base64.urlsafe_b64encode( + compute_hmac(shared_secret, self._servicer.received_message_bytes()) + ) + actual_public_key = _get_value_from_tuples( + _PUBLIC_KEY_HEADER, received_metadata ) - expected_client_metadata = ( - ( - _PUBLIC_KEY_HEADER, - base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) - ), - ), - ( - _AUTH_TOKEN_HEADER, - base64.urlsafe_b64encode(expected_hmac), - ), + actual_hmac = _get_value_from_tuples(_AUTH_TOKEN_HEADER, received_metadata) + expected_public_key = base64.urlsafe_b64encode( + public_key_to_bytes(self._client_public_key) ) # Assert - assert self._servicer.received_client_metadata() == expected_client_metadata + assert actual_public_key == expected_public_key + assert actual_hmac == expected_hmac def test_client_auth_get_run(self) -> None: """Test client authentication during send node.""" @@ -345,30 +388,32 @@ def test_client_auth_get_run(self) -> None: None, (self._client_private_key, self._client_public_key), ) as conn: - _, _, _, _, get_run, _ = conn + _, _, create_node, _, get_run, _ = conn + assert create_node is not None + create_node() assert get_run is not None get_run(0) + + received_metadata = self._servicer.received_client_metadata() + assert received_metadata is not None + shared_secret = generate_shared_key( self._servicer.server_private_key, self._client_public_key ) - expected_hmac = compute_hmac( - shared_secret, self._servicer.received_message_bytes() + expected_hmac = base64.urlsafe_b64encode( + compute_hmac(shared_secret, self._servicer.received_message_bytes()) + ) + actual_public_key = _get_value_from_tuples( + _PUBLIC_KEY_HEADER, received_metadata ) - expected_client_metadata = ( - ( - _PUBLIC_KEY_HEADER, - base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) - ), - ), - ( - _AUTH_TOKEN_HEADER, - base64.urlsafe_b64encode(expected_hmac), - ), + actual_hmac = _get_value_from_tuples(_AUTH_TOKEN_HEADER, received_metadata) + expected_public_key = base64.urlsafe_b64encode( + public_key_to_bytes(self._client_public_key) ) # Assert - assert self._servicer.received_client_metadata() == expected_client_metadata + assert actual_public_key == expected_public_key + assert actual_hmac == expected_hmac if __name__ == "__main__": From 250259e9948823f68bab39e06c76362e3b9888bc Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 20 Aug 2024 12:23:13 +0100 Subject: [PATCH 097/188] refactor(framework:skip) Remove non-public docstrings for `run_simulation` (#4048) --- src/py/flwr/simulation/run_simulation.py | 67 +----------------------- 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 257a066433fa..362fb3144053 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -425,72 +425,7 @@ def _run_simulation( verbose_logging: bool = False, is_app: bool = False, ) -> None: - r"""Launch the Simulation Engine. - - Parameters - ---------- - num_supernodes : int - Number of nodes that run a ClientApp. They can be sampled by a - Driver in the ServerApp and receive a Message describing what the ClientApp - should perform. - - client_app : Optional[ClientApp] - The `ClientApp` to be executed by each of the `SuperNodes`. It will receive - messages sent by the `ServerApp`. - - server_app : Optional[ServerApp] - The `ServerApp` to be executed. - - backend_name : str (default: ray) - A simulation backend that runs `ClientApp`s. - - backend_config : Optional[BackendConfig] - 'A dictionary to configure a backend. Separate dictionaries to configure - different elements of backend. Supported top-level keys are `init_args` - for values parsed to initialisation of backend, `client_resources` - to define the resources for clients, and `actor` to define the actor - parameters. Values supported in are those included by - `flwr.common.typing.ConfigsRecordValues`. - - client_app_attr : Optional[str] - A path to a `ClientApp` module to be loaded: For example: `client:app` or - `project.package.module:wrapper.app`." - - server_app_attr : Optional[str] - A path to a `ServerApp` module to be loaded: For example: `server:app` or - `project.package.module:wrapper.app`." - - server_app_run_config : Optional[UserConfig] - Config dictionary that parameterizes the run config. It will be made accesible - to the ServerApp. - - app_dir : str - Add specified directory to the PYTHONPATH and load `ClientApp` from there. - (Default: current working directory.) - - flwr_dir : Optional[str] - The path containing installed Flower Apps. - - run : Optional[Run] - An object carrying details about the run. - - enable_tf_gpu_growth : bool (default: False) - A boolean to indicate whether to enable GPU growth on the main thread. This is - desirable if you make use of a TensorFlow model on your `ServerApp` while - having your `ClientApp` running on the same GPU. Without enabling this, you - might encounter an out-of-memory error because TensorFlow by default allocates - all GPU memory. Read mor about how `tf.config.experimental.set_memory_growth()` - works in the TensorFlow documentation: https://www.tensorflow.org/api/stable. - - verbose_logging : bool (default: False) - When disabled, only INFO, WARNING and ERROR log messages will be shown. If - enabled, DEBUG-level logs will be displayed. - - is_app : bool (default: False) - A flag that indicates whether the simulation is running an app or not. This is - needed in order to attempt loading an app's pyproject.toml when nodes register - a context object. - """ + """Launch the Simulation Engine.""" if backend_config is None: backend_config = {} From 063220ced0e7725ecc33036bca41c2d5e74ec034 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 21 Aug 2024 16:44:06 +0200 Subject: [PATCH 098/188] break(framework) Use spaces instead of commas for separating config args (#4000) Co-authored-by: Javier --- src/py/flwr/cli/run/run.py | 9 ++++----- src/py/flwr/client/supernode/app.py | 4 ++-- src/py/flwr/common/config.py | 18 +++++++++++------- src/py/flwr/common/config_test.py | 2 +- src/py/flwr/superexec/app.py | 6 +++--- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index 2df14969e24e..ff7aeac226b2 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -56,7 +56,8 @@ def run( "--run-config", "-c", help="Override configuration key-value pairs, should be of the format:\n\n" - "`--run-config key1=value1,key2=value2 --run-config key3=value3`\n\n" + '`--run-config \'key1="value1" key2="value2"\' ' + "--run-config 'key3=\"value3\"'`\n\n" "Note that `key1`, `key2`, and `key3` in this example need to exist " "inside the `pyproject.toml` in order to be properly overriden.", ), @@ -171,9 +172,7 @@ def _run_with_superexec( req = StartRunRequest( fab=fab_to_proto(fab), - override_config=user_config_to_proto( - parse_config_args(config_overrides, separator=",") - ), + override_config=user_config_to_proto(parse_config_args(config_overrides)), federation_config=user_config_to_proto( flatten_dict(federation_config.get("options")) ), @@ -214,7 +213,7 @@ def _run_without_superexec( ] if config_overrides: - command.extend(["--run-config", f"{','.join(config_overrides)}"]) + command.extend(["--run-config", f"{' '.join(config_overrides)}"]) # Run the simulation subprocess.run( diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 2e6b942b5d2f..1b027d534a50 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -325,9 +325,9 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--node-config", type=str, - help="A comma separated list of key/value pairs (separated by `=`) to " + help="A space separated list of key/value pairs (separated by `=`) to " "configure the SuperNode. " - "E.g. --node-config 'key1=\"value1\",partition-id=0,num-partitions=100'", + "E.g. --node-config 'key1=\"value1\" partition-id=0 num-partitions=100'", ) diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index a319b3cdc704..c9bf5f31b83d 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -15,6 +15,7 @@ """Provide functions for managing global Flower config.""" import os +import re from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union, cast, get_args @@ -165,7 +166,6 @@ def unflatten_dict(flat_dict: Dict[str, Any]) -> Dict[str, Any]: def parse_config_args( config: Optional[List[str]], - separator: str = ",", ) -> UserConfig: """Parse separator separated list of key-value pairs separated by '='.""" overrides: UserConfig = {} @@ -173,18 +173,22 @@ def parse_config_args( if config is None: return overrides + # Regular expression to capture key-value pairs with possible quoted values + pattern = re.compile(r"(\S+?)=(\'[^\']*\'|\"[^\"]*\"|\S+)") + for config_line in config: if config_line: - overrides_list = config_line.split(separator) + matches = pattern.findall(config_line) + if ( - len(overrides_list) == 1 - and "=" not in overrides_list - and overrides_list[0].endswith(".toml") + len(matches) == 1 + and "=" not in matches[0][0] + and matches[0][0].endswith(".toml") ): - with Path(overrides_list[0]).open("rb") as config_file: + with Path(matches[0][0]).open("rb") as config_file: overrides = flatten_dict(tomli.load(config_file)) else: - toml_str = "\n".join(overrides_list) + toml_str = "\n".join(f"{k} = {v}" for k, v in matches) overrides.update(tomli.loads(toml_str)) return overrides diff --git a/src/py/flwr/common/config_test.py b/src/py/flwr/common/config_test.py index 071263ed8531..712e07264d3f 100644 --- a/src/py/flwr/common/config_test.py +++ b/src/py/flwr/common/config_test.py @@ -245,7 +245,7 @@ def test_parse_config_args_none() -> None: def test_parse_config_args_overrides() -> None: """Test parse_config_args with key-value pairs.""" assert parse_config_args( - ["key1='value1',key2='value2'", "key3=1", "key4=2.0,key5=true,key6='value6'"] + ["key1='value1' key2='value2'", "key3=1", "key4=2.0 key5=true key6='value6'"] ) == { "key1": "value1", "key2": "value2", diff --git a/src/py/flwr/superexec/app.py b/src/py/flwr/superexec/app.py index 2ad5f12d227f..9510479ec8e1 100644 --- a/src/py/flwr/superexec/app.py +++ b/src/py/flwr/superexec/app.py @@ -93,9 +93,9 @@ def _parse_args_run_superexec() -> argparse.ArgumentParser: ) parser.add_argument( "--executor-config", - help="Key-value pairs for the executor config, separated by commas. " - 'For example:\n\n`--executor-config superlink="superlink:9091",' - 'root-certificates="certificates/superlink-ca.crt"`', + help="Key-value pairs for the executor config, separated by spaces. " + 'For example:\n\n`--executor-config \'superlink="superlink:9091" ' + 'root-certificates="certificates/superlink-ca.crt"\'`', ) parser.add_argument( "--insecure", From 0e71988460b5d9dcc4f1a4dc5eddccab3e0499c5 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 21 Aug 2024 15:56:15 +0100 Subject: [PATCH 099/188] refactor(framework:skip) Pass `sys.path` to ray processes (#3985) Co-authored-by: Javier --- .../flwr/server/superlink/fleet/vce/backend/raybackend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py index 2087a88360c4..0ef2ef737a8f 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py @@ -14,6 +14,7 @@ # ============================================================================== """Ray backend for the Fleet API using the Simulation Engine.""" +import sys from logging import DEBUG, ERROR from typing import Callable, Dict, Tuple, Union @@ -111,8 +112,10 @@ def init_ray(self, backend_config: BackendConfig) -> None: if backend_config.get(self.init_args_key): for k, v in backend_config[self.init_args_key].items(): ray_init_args[k] = v - - ray.init(**ray_init_args) + ray.init( + runtime_env={"env_vars": {"PYTHONPATH": ":".join(sys.path)}}, + **ray_init_args, + ) @property def num_workers(self) -> int: From b4419ed434cfc30a2bbc95d67191db502aa29e52 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 16:53:51 +0100 Subject: [PATCH 100/188] fix(framework:skip) Enable `SuperNode` to complete context registration when `FAB` is not installed (#4049) --- src/py/flwr/client/app.py | 6 ++++-- src/py/flwr/client/node_state.py | 21 +++++++++++++++++---- src/py/flwr/common/config.py | 14 +++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 1aed5d5241dd..fb4855a09817 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -452,12 +452,14 @@ def _on_backoff(retry_state: RetryState) -> None: # Register context for this run node_state.register_context( - run_id=run_id, run=run, flwr_path=flwr_path + run_id=run_id, + run=run, + flwr_path=flwr_path, + fab=fab, ) # Retrieve context for this run context = node_state.retrieve_context(run_id=run_id) - # Create an error reply message that will never be used to prevent # the used-before-assignment linting error reply_message = message.create_error_reply( diff --git a/src/py/flwr/client/node_state.py b/src/py/flwr/client/node_state.py index 3320c90cb8cc..e16d7e34715d 100644 --- a/src/py/flwr/client/node_state.py +++ b/src/py/flwr/client/node_state.py @@ -20,8 +20,12 @@ from typing import Dict, Optional from flwr.common import Context, RecordSet -from flwr.common.config import get_fused_config, get_fused_config_from_dir -from flwr.common.typing import Run, UserConfig +from flwr.common.config import ( + get_fused_config, + get_fused_config_from_dir, + get_fused_config_from_fab, +) +from flwr.common.typing import Fab, Run, UserConfig @dataclass() @@ -44,12 +48,14 @@ def __init__( self.node_config = node_config self.run_infos: Dict[int, RunInfo] = {} + # pylint: disable=too-many-arguments def register_context( self, run_id: int, run: Optional[Run] = None, flwr_path: Optional[Path] = None, app_dir: Optional[str] = None, + fab: Optional[Fab] = None, ) -> None: """Register new run context for this node.""" if run_id not in self.run_infos: @@ -65,8 +71,15 @@ def register_context( else: raise ValueError("The specified `app_dir` must be a directory.") else: - # Load from .fab - initial_run_config = get_fused_config(run, flwr_path) if run else {} + if run: + if fab: + # Load pyproject.toml from FAB file and fuse + initial_run_config = get_fused_config_from_fab(fab.content, run) + else: + # Load pyproject.toml from installed FAB and fuse + initial_run_config = get_fused_config(run, flwr_path) + else: + initial_run_config = {} self.run_infos[run_id] = RunInfo( initial_run_config=initial_run_config, context=Context( diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index c9bf5f31b83d..eec7cfb726b7 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -21,7 +21,7 @@ import tomli -from flwr.cli.config_utils import validate_fields +from flwr.cli.config_utils import get_fab_config, validate_fields from flwr.common.constant import APP_DIR, FAB_CONFIG_FILE, FLWR_HOME from flwr.common.typing import Run, UserConfig, UserConfigValue @@ -105,6 +105,18 @@ def get_fused_config_from_dir( return fuse_dicts(flat_default_config, override_config) +def get_fused_config_from_fab(fab_file: Union[Path, bytes], run: Run) -> UserConfig: + """Fuse default config in a `FAB` with overrides in a `Run`. + + This enables obtaining a run-config without having to install the FAB. This + function mirrors `get_fused_config_from_dir`. This is useful when the execution + of the FAB is delegated to a different process. + """ + default_config = get_fab_config(fab_file)["tool"]["flwr"]["app"].get("config", {}) + flat_config_flat = flatten_dict(default_config) + return fuse_dicts(flat_config_flat, run.override_config) + + def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig: """Merge the overrides from a `Run` with the config from a FAB. From 29827fcd088cf249f47651fa05ebe5ae5122a1ac Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 19:00:30 +0100 Subject: [PATCH 101/188] feat(framework:skip) Export `MetricsRecordValues` and `ConfigsRecordValues` (#4052) --- src/py/flwr/common/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/py/flwr/common/__init__.py b/src/py/flwr/common/__init__.py index bbdf48425e0a..925f21ddb491 100644 --- a/src/py/flwr/common/__init__.py +++ b/src/py/flwr/common/__init__.py @@ -41,6 +41,7 @@ from .typing import ClientMessage as ClientMessage from .typing import Code as Code from .typing import Config as Config +from .typing import ConfigsRecordValues as ConfigsRecordValues from .typing import DisconnectRes as DisconnectRes from .typing import EvaluateIns as EvaluateIns from .typing import EvaluateRes as EvaluateRes @@ -52,6 +53,7 @@ from .typing import GetPropertiesRes as GetPropertiesRes from .typing import Metrics as Metrics from .typing import MetricsAggregationFn as MetricsAggregationFn +from .typing import MetricsRecordValues as MetricsRecordValues from .typing import NDArray as NDArray from .typing import NDArrays as NDArrays from .typing import Parameters as Parameters @@ -67,6 +69,7 @@ "Code", "Config", "ConfigsRecord", + "ConfigsRecordValues", "Context", "DEFAULT_TTL", "DisconnectRes", @@ -88,6 +91,7 @@ "Metrics", "MetricsAggregationFn", "MetricsRecord", + "MetricsRecordValues", "NDArray", "NDArrays", "Parameters", From 53176af390f086c32bfce298ef9874800b112703 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 19:13:37 +0100 Subject: [PATCH 102/188] docs(framework) Add examples to records docstrings (#4021) Co-authored-by: Heng Pan --- src/py/flwr/common/record/configsrecord.py | 64 ++++++++--- src/py/flwr/common/record/metricsrecord.py | 68 +++++++++--- src/py/flwr/common/record/parametersrecord.py | 101 +++++++++++++++--- src/py/flwr/common/record/recordset.py | 72 ++++++++++++- 4 files changed, 258 insertions(+), 47 deletions(-) diff --git a/src/py/flwr/common/record/configsrecord.py b/src/py/flwr/common/record/configsrecord.py index 471c85f0b961..aeb311089bcd 100644 --- a/src/py/flwr/common/record/configsrecord.py +++ b/src/py/flwr/common/record/configsrecord.py @@ -58,27 +58,61 @@ def is_valid(__v: ConfigsScalar) -> None: class ConfigsRecord(TypedDict[str, ConfigsRecordValues]): - """Configs record.""" + """Configs record. + + A :code:`ConfigsRecord` is a Python dictionary designed to ensure that + each key-value pair adheres to specified data types. A :code:`ConfigsRecord` + is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + configs_dict : Optional[Dict[str, ConfigsRecordValues]] + A dictionary that stores basic types (i.e. `str`, `int`, `float`, `bytes` as + defined in `ConfigsScalar`) and lists of such types (see + `ConfigsScalarList`). + keep_input : bool (default: True) + A boolean indicating whether config passed should be deleted from the input + dictionary immediately after adding them to the record. When set + to True, the data is duplicated in memory. If memory is a concern, set + it to False. + + Examples + -------- + The usage of a :code:`ConfigsRecord` is envisioned for sending configuration values + telling the target node how to perform a certain action (e.g. train/evaluate a model + ). You can use standard Python built-in types such as :code:`float`, :code:`str` + , :code:`bytes`. All types allowed are defined in + :code:`flwr.common.ConfigsRecordValues`. While lists are supported, we + encourage you to use a :code:`ParametersRecord` instead if these are of high + dimensionality. + + Let's see some examples of how to construct a :code:`ConfigsRecord` from scratch: + + >>> from flwr.common import ConfigsRecord + >>> + >>> # A `ConfigsRecord` is a specialized Python dictionary + >>> record = ConfigsRecord({"lr": 0.1, "batch-size": 128}) + >>> # You can add more content to an existing record + >>> record["compute-average"] = True + >>> # It also supports lists + >>> record["loss-fn-coefficients"] = [0.4, 0.25, 0.35] + >>> # And string values (among other types) + >>> record["path-to-S3"] = "s3://bucket_name/folder1/fileA.json" + + Just like the other types of records in a :code:`flwr.common.RecordSet`, types are + enforced. If you need to add a custom data structure or object, we recommend to + serialise it into bytes and save it as such (bytes are allowed in a + :code:`ConfigsRecord`) + """ def __init__( self, configs_dict: Optional[Dict[str, ConfigsRecordValues]] = None, keep_input: bool = True, ) -> None: - """Construct a ConfigsRecord object. - - Parameters - ---------- - configs_dict : Optional[Dict[str, ConfigsRecordValues]] - A dictionary that stores basic types (i.e. `str`, `int`, `float`, `bytes` as - defined in `ConfigsScalar`) and lists of such types (see - `ConfigsScalarList`). - keep_input : bool (default: True) - A boolean indicating whether config passed should be deleted from the input - dictionary immediately after adding them to the record. When set - to True, the data is duplicated in memory. If memory is a concern, set - it to False. - """ + super().__init__(_check_key, _check_value) if configs_dict: for k in list(configs_dict.keys()): diff --git a/src/py/flwr/common/record/metricsrecord.py b/src/py/flwr/common/record/metricsrecord.py index 2b6e584be390..868ed82e79ca 100644 --- a/src/py/flwr/common/record/metricsrecord.py +++ b/src/py/flwr/common/record/metricsrecord.py @@ -58,26 +58,66 @@ def is_valid(__v: MetricsScalar) -> None: class MetricsRecord(TypedDict[str, MetricsRecordValues]): - """Metrics record.""" + """Metrics recod. + + A :code:`MetricsRecord` is a Python dictionary designed to ensure that + each key-value pair adheres to specified data types. A :code:`MetricsRecord` + is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + metrics_dict : Optional[Dict[str, MetricsRecordValues]] + A dictionary that stores basic types (i.e. `int`, `float` as defined + in `MetricsScalar`) and list of such types (see `MetricsScalarList`). + keep_input : bool (default: True) + A boolean indicating whether metrics should be deleted from the input + dictionary immediately after adding them to the record. When set + to True, the data is duplicated in memory. If memory is a concern, set + it to False. + + Examples + -------- + The usage of a :code:`MetricsRecord` is envisioned for communicating results + obtained when a node performs an action. A few typical examples include: + communicating the training accuracy after a model is trained locally by a + :code:`ClientApp`, reporting the validation loss obtained at a :code:`ClientApp`, + or, more generally, the output of executing a query by the :code:`ClientApp`. + Common to these examples is that the output can be typically represented by + a single scalar (:code:`int`, :code:`float`) or list of scalars. + + Let's see some examples of how to construct a :code:`MetricsRecord` from scratch: + + >>> from flwr.common import MetricsRecord + >>> + >>> # A `MetricsRecord` is a specialized Python dictionary + >>> record = MetricsRecord({"accuracy": 0.94}) + >>> # You can add more content to an existing record + >>> record["loss"] = 0.01 + >>> # It also supports lists + >>> record["loss-historic"] = [0.9, 0.5, 0.01] + + Since types are enforced, the types of the objects inserted are checked. For a + :code:`MetricsRecord`, value types allowed are those in defined in + :code:`flwr.common.MetricsRecordValues`. Similarly, only :code:`str` keys are + allowed. + + >>> from flwr.common import MetricsRecord + >>> + >>> record = MetricsRecord() # an empty record + >>> # Add unsupported value + >>> record["something-unsupported"] = {'a': 123} # Will throw a `TypeError` + + If you need a more versatily type of record try :code:`ConfigsRecord` or + :code:`ParametersRecord`. + """ def __init__( self, metrics_dict: Optional[Dict[str, MetricsRecordValues]] = None, keep_input: bool = True, ): - """Construct a MetricsRecord object. - - Parameters - ---------- - metrics_dict : Optional[Dict[str, MetricsRecordValues]] - A dictionary that stores basic types (i.e. `int`, `float` as defined - in `MetricsScalar`) and list of such types (see `MetricsScalarList`). - keep_input : bool (default: True) - A boolean indicating whether metrics should be deleted from the input - dictionary immediately after adding them to the record. When set - to True, the data is duplicated in memory. If memory is a concern, set - it to False. - """ super().__init__(_check_key, _check_value) if metrics_dict: for k in list(metrics_dict.keys()): diff --git a/src/py/flwr/common/record/parametersrecord.py b/src/py/flwr/common/record/parametersrecord.py index 93db6d387b53..f088d682497b 100644 --- a/src/py/flwr/common/record/parametersrecord.py +++ b/src/py/flwr/common/record/parametersrecord.py @@ -83,11 +83,93 @@ def _check_value(value: Array) -> None: class ParametersRecord(TypedDict[str, Array]): - """Parameters record. + r"""Parameters record. A dataclass storing named Arrays in order. This means that it holds entries as an OrderedDict[str, Array]. ParametersRecord objects can be viewed as an equivalent to - PyTorch's state_dict, but holding serialised tensors instead. + PyTorch's state_dict, but holding serialised tensors instead. A + :code:`ParametersRecord` is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + array_dict : Optional[OrderedDict[str, Array]] + A dictionary that stores serialized array-like or tensor-like objects. + keep_input : bool (default: False) + A boolean indicating whether parameters should be deleted from the input + dictionary immediately after adding them to the record. If False, the + dictionary passed to `set_parameters()` will be empty once exiting from that + function. This is the desired behaviour when working with very large + models/tensors/arrays. However, if you plan to continue working with your + parameters after adding it to the record, set this flag to True. When set + to True, the data is duplicated in memory. + + Examples + -------- + The usage of :code:`ParametersRecord` is envisioned for storing data arrays (e.g. + parameters of a machine learning model). These first need to be serialized into + a :code:`flwr.common.Array` data structure. + + Let's see some examples: + + >>> import numpy as np + >>> from flwr.common import ParametersRecord + >>> from flwr.common import array_from_numpy + >>> + >>> # Let's create a simple NumPy array + >>> arr_np = np.random.randn(3, 3) + >>> + >>> # If we print it + >>> array([[-1.84242409, -1.01539537, -0.46528405], + >>> [ 0.32991896, 0.55540414, 0.44085534], + >>> [-0.10758364, 1.97619858, -0.37120501]]) + >>> + >>> # Let's create an Array out of it + >>> arr = array_from_numpy(arr_np) + >>> + >>> # If we print it you'll see (note the binary data) + >>> Array(dtype='float64', shape=[3,3], stype='numpy.ndarray', data=b'@\x99\x18...') + >>> + >>> # Adding it to a ParametersRecord: + >>> p_record = ParametersRecord({"my_array": arr}) + + Now that the NumPy array is embedded into a :code:`ParametersRecord` it could be + sent if added as part of a :code:`common.Message` or it could be saved as a + persistent state of a :code:`ClientApp` via its context. Regardless of the usecase, + we will sooner or later want to recover the array in its original NumPy + representation. For the example above, where the array was serialized using the + built-in utility function, deserialization can be done as follows: + + >>> # Use the Array's built-in method + >>> arr_np_d = arr.numpy() + >>> + >>> # If printed, it will show the exact same data as above: + >>> array([[-1.84242409, -1.01539537, -0.46528405], + >>> [ 0.32991896, 0.55540414, 0.44085534], + >>> [-0.10758364, 1.97619858, -0.37120501]]) + + If you need finer control on how your arrays are serialized and deserialized, you + can construct :code:`Array` objects directly like this: + + >>> from flwr.common import Array + >>> # Serialize your array and construct Array object + >>> arr = Array( + >>> data=ndarray.tobytes(), + >>> dtype=str(ndarray.dtype), + >>> stype="", # Could be used in a deserialization function + >>> shape=list(ndarray.shape), + >>> ) + >>> + >>> # Then you can deserialize it like this + >>> arr_np_d = np.frombuffer( + >>> buffer=array.data, + >>> dtype=array.dtype, + >>> ).reshape(array.shape) + + Note that different arrays (e.g. from PyTorch, Tensorflow) might require different + serialization mechanism. Howerver, they often support a conversion to NumPy, + therefore allowing to use the same or similar steps as in the example above. """ def __init__( @@ -95,21 +177,6 @@ def __init__( array_dict: Optional[OrderedDict[str, Array]] = None, keep_input: bool = False, ) -> None: - """Construct a ParametersRecord object. - - Parameters - ---------- - array_dict : Optional[OrderedDict[str, Array]] - A dictionary that stores serialized array-like or tensor-like objects. - keep_input : bool (default: False) - A boolean indicating whether parameters should be deleted from the input - dictionary immediately after adding them to the record. If False, the - dictionary passed to `set_parameters()` will be empty once exiting from that - function. This is the desired behaviour when working with very large - models/tensors/arrays. However, if you plan to continue working with your - parameters after adding it to the record, set this flag to True. When set - to True, the data is duplicated in memory. - """ super().__init__(_check_key, _check_value) if array_dict: for k in list(array_dict.keys()): diff --git a/src/py/flwr/common/record/recordset.py b/src/py/flwr/common/record/recordset.py index 098b73b2d429..f16a22695d6e 100644 --- a/src/py/flwr/common/record/recordset.py +++ b/src/py/flwr/common/record/recordset.py @@ -86,7 +86,77 @@ def _check_fn_configs(self, record: ConfigsRecord) -> None: class RecordSet: - """RecordSet stores groups of parameters, metrics and configs.""" + """RecordSet stores groups of parameters, metrics and configs. + + A :code:`RecordSet` is the unified mechanism by which parameters, + metrics and configs can be either stored as part of a + `flwr.common.Context `_ in your apps + or communicated as part of a + `flwr.common.Message `_ between your apps. + + Parameters + ---------- + parameters_records : Optional[Dict[str, ParametersRecord]] + A dictionary of :code:`ParametersRecords` that can be used to record + and communicate model parameters and high-dimensional arrays. + metrics_records : Optional[Dict[str, MetricsRecord]] + A dictionary of :code:`MetricsRecord` that can be used to record + and communicate scalar-valued metrics that are the result of performing + and action, for example, by a :code:`ClientApp`. + configs_records : Optional[Dict[str, ConfigsRecord]] + A dictionary of :code:`ConfigsRecord` that can be used to record + and communicate configuration values to an entity (e.g. to a + :code:`ClientApp`) + for it to adjust how an action is performed. + + Examples + -------- + A :code:`RecordSet` can hold three types of records, each designed + with an specific purpose. What is common to all of them is that they + are Python dictionaries designed to ensure that each key-value pair + adheres to specified data types. + + Let's see an example. + + >>> from flwr.common import RecordSet + >>> from flwr.common import ConfigsRecords, MetricsRecords, ParametersRecord + >>> + >>> # Let's begin with an empty record + >>> my_recordset = RecordSet() + >>> + >>> # We can create a ConfigsRecord + >>> c_record = ConfigsRecord({"lr": 0.1, "batch-size": 128}) + >>> # Adding it to the record_set would look like this + >>> my_recordset.configs_records["my_config"] = c_record + >>> + >>> # We can create a MetricsRecord following a similar process + >>> m_record = MetricsRecord({"accuracy": 0.93, "losses": [0.23, 0.1]}) + >>> # Adding it to the record_set would look like this + >>> my_recordset.metrics_records["my_metrics"] = m_record + + Adding a :code:`ParametersRecord` follows the same steps as above but first, + the array needs to be serialized and represented as a :code:`flwr.common.Array`. + If the array is a :code:`NumPy` array, you can use the built-in utility function + `array_from_numpy `_. It is often possible to + convert an array first to :code:`NumPy` and then use the aforementioned function. + + >>> from flwr.common import array_from_numpy + >>> # Creating a ParametersRecord would look like this + >>> arr_np = np.random.randn(3, 3) + >>> + >>> # You can use the built-in tool to serialize the array + >>> arr = array_from_numpy(arr_np) + >>> + >>> # Finally, create the record + >>> p_record = ParametersRecord({"my_array": arr}) + >>> + >>> # Adding it to the record_set would look like this + >>> my_recordset.configs_records["my_config"] = c_record + + For additional examples on how to construct each of the records types shown + above, please refer to the documentation for :code:`ConfigsRecord`, + :code:`MetricsRecord` and :code:`ParametersRecord`. + """ def __init__( self, From abde02e3c900aad78c2a156866746952257865fe Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Wed, 21 Aug 2024 21:26:37 +0200 Subject: [PATCH 103/188] docs(*:skip) Update Docker documentation (#3722) Signed-off-by: Robert Steiner --- doc/source/conf.py | 4 + ...contributor-how-to-build-docker-images.rst | 72 ++- doc/source/docker/enable-tls.rst | 152 ++++++ doc/source/docker/index.rst | 37 ++ doc/source/docker/persist-superlink-state.rst | 39 ++ doc/source/docker/pin-version.rst | 36 ++ doc/source/docker/run-as-root-user.rst | 45 ++ .../docker/set-environment-variables.rst | 14 + .../docker/tutorial-quickstart-docker.rst | 369 +++++++++++++ doc/source/docker/use-a-different-version.rst | 12 + doc/source/how-to-install-flower.rst | 2 +- doc/source/how-to-run-flower-using-docker.rst | 501 ------------------ doc/source/index.rst | 2 +- 13 files changed, 743 insertions(+), 542 deletions(-) create mode 100644 doc/source/docker/enable-tls.rst create mode 100644 doc/source/docker/index.rst create mode 100644 doc/source/docker/persist-superlink-state.rst create mode 100644 doc/source/docker/pin-version.rst create mode 100644 doc/source/docker/run-as-root-user.rst create mode 100644 doc/source/docker/set-environment-variables.rst create mode 100644 doc/source/docker/tutorial-quickstart-docker.rst create mode 100644 doc/source/docker/use-a-different-version.rst delete mode 100644 doc/source/how-to-run-flower-using-docker.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 27cae3f92e1f..d3881325a5ce 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -94,6 +94,10 @@ # The current released version rst_prolog = """ .. |stable_flwr_version| replace:: 1.10.0 +.. |stable_flwr_superlink_docker_digest| replace:: 4b317d5b6030710b476f4dbfab2c3a33021ad40a0fcfa54d7edd45e0c51d889c +.. |ubuntu_version| replace:: 22.04 +.. |setuptools_version| replace:: 70.3.0 +.. |pip_version| replace:: 24.1.2 """ # -- General configuration --------------------------------------------------- diff --git a/doc/source/contributor-how-to-build-docker-images.rst b/doc/source/contributor-how-to-build-docker-images.rst index 457193c93db6..522d124dfd9b 100644 --- a/doc/source/contributor-how-to-build-docker-images.rst +++ b/doc/source/contributor-how-to-build-docker-images.rst @@ -1,4 +1,4 @@ -How to build Docker Flower images locally +How to Build Docker Flower Images Locally ========================================= Flower provides pre-made docker images on `Docker Hub `_ @@ -9,27 +9,22 @@ images exist and how to build them locally. Before we can start, we need to meet a few prerequisites in our local development environment. -#. Clone the flower repository. +#. Clone the ``flower`` repository. .. code-block:: bash - $ git clone https://github.com/adap/flower.git && cd flower + $ git clone --depth=1 https://github.com/adap/flower.git && cd flower #. Verify the Docker daemon is running. - Please follow the first section on - :doc:`Run Flower using Docker ` - which covers this step in more detail. + The build instructions that assemble the images are located in the respective Dockerfiles. You + can find them in the subdirectories of ``src/docker``. - -The build instructions that assemble the images are located in the respective Dockerfiles. You -can find them in the subdirectories of ``src/docker``. - -Flower Docker images are configured via build arguments. Through build arguments, we can make the -creation of images more flexible. For example, in the base image, we can specify the version of -Python to install using the ``PYTHON_VERSION`` build argument. Some of the build arguments have -default values, others must be specified when building the image. All available build arguments for -each image are listed in one of the tables below. + Flower Docker images are configured via build arguments. Through build arguments, we can make the + creation of images more flexible. For example, in the base image, we can specify the version of + Python to install using the ``PYTHON_VERSION`` build argument. Some of the build arguments have + default values, others must be specified when building the image. All available build arguments for + each image are listed in one of the tables below. Building the base image ----------------------- @@ -49,7 +44,7 @@ Building the base image * - ``DISTRO_VERSION`` - Version of the Linux distribution. - No - - ``22.04`` + - :substitution-code:`|ubuntu_version|` * - ``PYTHON_VERSION`` - Version of ``python`` to be installed. - No @@ -57,11 +52,11 @@ Building the base image * - ``PIP_VERSION`` - Version of ``pip`` to be installed. - Yes - - ``23.0.1`` + - :substitution-code:`|pip_version|` * - ``SETUPTOOLS_VERSION`` - Version of ``setuptools`` to be installed. - Yes - - ``69.0.2`` + - :substitution-code:`|setuptools_version|` * - ``FLWR_VERSION`` - Version of Flower to be installed. - Yes @@ -71,9 +66,9 @@ Building the base image - No - ``flwr`` or ``flwr-nightly`` - -The following example creates a base Ubuntu/Alpine image with Python 3.11.0, pip 23.0.1, -setuptools 69.0.2 and Flower |stable_flwr_version|: +The following example creates a base Ubuntu/Alpine image with Python ``3.11.0``, +pip :substitution-code:`|pip_version|`, setuptools :substitution-code:`|setuptools_version|` +and Flower :substitution-code:`|stable_flwr_version|`: .. code-block:: bash :substitutions: @@ -82,11 +77,11 @@ setuptools 69.0.2 and Flower |stable_flwr_version|: $ docker build \ --build-arg PYTHON_VERSION=3.11.0 \ --build-arg FLWR_VERSION=|stable_flwr_version| \ - --build-arg PIP_VERSION=23.0.1 \ - --build-arg SETUPTOOLS_VERSION=69.0.2 \ + --build-arg PIP_VERSION=|pip_version| \ + --build-arg SETUPTOOLS_VERSION=|setuptools_version| \ -t flwr_base:0.1.0 . -The name of image is ``flwr_base`` and the tag ``0.1.0``. Remember that the build arguments as well +In this example, we specify our image name as ``flwr_base`` and the tag as ``0.1.0``. Remember that the build arguments as well as the name and tag can be adapted to your needs. These values serve as examples only. Building the SuperLink/SuperNode or ServerApp image @@ -107,32 +102,31 @@ Building the SuperLink/SuperNode or ServerApp image * - ``BASE_IMAGE`` - The Tag of the Flower base image. - Yes - - :substitution-code:`|stable_flwr_version|-py3.10-ubuntu22.04` + - :substitution-code:`|stable_flwr_version|-py3.11-ubuntu|ubuntu_version|` -The following example creates a SuperLink/SuperNode or ServerApp image with the official Flower -base image: +For example, to build a SuperLink image with the latest Flower version, Python 3.11 and Ubuntu 22.04, run the following: .. code-block:: bash + :substitutions: - $ cd src/docker// - $ docker build \ - --build-arg BASE_IMAGE=-py- \ - -t flwr_superlink:0.1.0 . - + $ cd src/docker/superlink + $ docker build \ + --build-arg BASE_IMAGE=|stable_flwr_version|-py3.11-ubuntu22.04 \ + -t flwr_superlink:0.1.0 . If you want to use your own base image instead of the official Flower base image, all you need to do -is set the ``BASE_REPOSITORY`` build argument. +is set the ``BASE_REPOSITORY`` build argument to ``flwr_base`` (as we've specified above). .. code-block:: bash - $ cd src/docker/superlink/ - $ docker build \ - --build-arg BASE_REPOSITORY=flwr_base \ - --build-arg BASE_IMAGE=0.1.0 - -t flwr_superlink:0.1.0 . + $ cd src/docker/superlink/ + $ docker build \ + --build-arg BASE_REPOSITORY=flwr_base \ + --build-arg BASE_IMAGE=0.1.0 + -t flwr_superlink:0.1.0 . After creating the image, we can test whether the image is working: .. code-block:: bash - $ docker run --rm flwr_superlink:0.1.0 --help + $ docker run --rm flwr_superlink:0.1.0 --help diff --git a/doc/source/docker/enable-tls.rst b/doc/source/docker/enable-tls.rst new file mode 100644 index 000000000000..ac604b708f88 --- /dev/null +++ b/doc/source/docker/enable-tls.rst @@ -0,0 +1,152 @@ +Enable TLS for Secure Connections +================================= + +When operating in a production environment, it is strongly recommended to enable Transport Layer +Security (TLS) for each Flower Component to ensure secure communication. + +To enable TLS, you will need a PEM-encoded root certificate, a PEM-encoded private key and a +PEM-encoded certificate chain. + +.. note:: + + For testing purposes, you can generate your own self-signed certificates. The + `Enable SSL connections `__ + page contains a section that will guide you through the process. + + +Because Flower containers, by default, run with a non-root user ``app``, the mounted files and +directories must have the proper permissions for the user ID ``49999``. + +For example, to change the user ID of all files in the ``certificates/`` directory, you can run +``sudo chown -R 49999:49999 certificates/*``. + +If you later want to delete the directory, you can change the user ID back to the current user +ID by running ``sudo chown -R $USER:$(id -gn) state``. + +SuperLink +--------- + +Assuming all files we need are in the local ``certificates`` directory, we can use the flag +``--volume`` to mount the local directory into the ``/app/certificates/`` directory of the container: + +.. code-block:: bash + :substitutions: + + $ docker run --rm \ + --volume ./certificates/:/app/certificates/:ro \ + flwr/superlink:|stable_flwr_version| \ + --ssl-ca-certfile certificates/ca.crt \ + --ssl-certfile certificates/server.pem \ + --ssl-keyfile certificates/server.key + +.. dropdown:: Understanding the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``--volume ./certificates/:/app/certificates/:ro``: Mount the ``certificates`` directory in + | the current working directory of the host machine as a read-only volume at the + | ``/app/certificates`` directory inside the container. + | + | This allows the container to access the TLS certificates that are stored in the certificates + | directory. + * | :substitution-code:`flwr/superlink:|stable_flwr_version|`: The name of the image to be run and the specific + | tag of the image. The tag :substitution-code:`|stable_flwr_version|` represents a specific version of the image. + * | ``--ssl-ca-certfile certificates/ca.crt``: Specify the location of the CA certificate file + | inside the container. + | + | The ``certificates/ca.crt`` file is a certificate that is used to verify the identity of the + | SuperLink. + * | ``--ssl-certfile certificates/server.pem``: Specify the location of the SuperLink's + | TLS certificate file inside the container. + | + | The ``certificates/server.pem`` file is used to identify the SuperLink and to encrypt the + | data that is transmitted over the network. + * | ``--ssl-keyfile certificates/server.key``: Specify the location of the SuperLink's + | TLS private key file inside the container. + | + | The ``certificates/server.key`` file is used to decrypt the data that is transmitted over + | the network. + +SuperNode +--------- + +Assuming that the ``ca.crt`` certificate already exists locally, we can use the flag ``--volume`` to mount the local +certificate into the container's ``/app/`` directory. + +.. note:: + + If you're generating self-signed certificates and the ``ca.crt`` certificate doesn't exist + on the SuperNode, you can copy it over after the generation step. + +.. code-block:: bash + :substitutions: + + $ docker run --rm \ + --volume ./ca.crt:/app/ca.crt/:ro \ + flwr/supernode:|stable_flwr_version| \ + --root-certificates ca.crt + +.. dropdown:: Understanding the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``--volume ./ca.crt:/app/ca.crt/:ro``: Mount the ``ca.crt`` file from the + | current working directory of the host machine as a read-only volume at the ``/app/ca.crt`` + | directory inside the container. + * | :substitution-code:`flwr/supernode:|stable_flwr_version|`: The name of the image to be run and the specific + | tag of the image. The tag :substitution-code:`|stable_flwr_version|` represents a specific version of the image. + * | ``--root-certificates ca.crt``: This specifies the location of the CA certificate file + | inside the container. + | + | The ``ca.crt`` file is used to verify the identity of the SuperLink. + + +SuperExec +--------- + +Assuming all files we need are in the local ``certificates`` directory where the SuperExec will be executed from, we can use the flag +``--volume`` to mount the local directory into the ``/app/certificates/`` directory of the container: + +.. code-block:: bash + :substitutions: + + $ docker run --rm \ + --volume ./certificates/:/app/certificates/:ro \ + flwr/superexec:|stable_flwr_version| \ + --ssl-ca-certfile certificates/ca.crt \ + --ssl-certfile certificates/server.pem \ + --ssl-keyfile certificates/server.key \ + --executor-config \ + root-certificates=\"certificates/superlink_ca.crt\" + + +.. dropdown:: Understanding the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``--volume ./certificates/:/app/certificates/:ro``: Mount the ``certificates`` directory in + | the current working directory of the host machine as a read-only volume at the + | ``/app/certificates`` directory inside the container. + | + | This allows the container to access the TLS certificates that are stored in the certificates + | directory. + * | :substitution-code:`flwr/superexec:|stable_flwr_version|`: The name of the image to be run and the specific + | tag of the image. The tag :substitution-code:`|stable_flwr_version|` represents a specific version of the image. + * | ``--ssl-ca-certfile certificates/ca.crt``: Specify the location of the CA certificate file + | inside the container. + | + | The ``certificates/ca.crt`` file is a certificate that is used to verify the identity of the + | SuperExec. + * | ``--ssl-certfile certificates/server.pem``: Specify the location of the SuperExec's + | TLS certificate file inside the container. + | + | The ``certificates/server.pem`` file is used to identify the SuperExec and to encrypt the + | data that is transmitted over the network. + * | ``--ssl-keyfile certificates/server.key``: Specify the location of the SuperExec's + | TLS private key file inside the container. + | + | The ``certificates/server.key`` file is used to decrypt the data that is transmitted over + | the network. + * | ``--executor-config root-certificates=\"certificates/superlink_ca.crt\"``: Specify the + | location of the CA certificate file inside the container that the SuperExec executor + | should use to verify the SuperLink's identity. diff --git a/doc/source/docker/index.rst b/doc/source/docker/index.rst new file mode 100644 index 000000000000..389c42a23067 --- /dev/null +++ b/doc/source/docker/index.rst @@ -0,0 +1,37 @@ +Run Flower using Docker +======================= + +Start your Flower journey with our pre-made Docker images on Docker Hub, supporting ``amd64`` +and ``arm64v8`` architectures. + +Our Quickstart guide walks you through containerizing a Flower project and running it end to +end using Docker. + +Getting Started +--------------- + +.. toctree:: + :maxdepth: 1 + + tutorial-quickstart-docker + + +Running in Production +--------------------- + +.. toctree:: + :maxdepth: 1 + + enable-tls + persist-superlink-state + +Advanced Options +---------------- + +.. toctree:: + :maxdepth: 1 + + set-environment-variables + run-as-root-user + pin-version + use-a-different-version diff --git a/doc/source/docker/persist-superlink-state.rst b/doc/source/docker/persist-superlink-state.rst new file mode 100644 index 000000000000..68e04ed33762 --- /dev/null +++ b/doc/source/docker/persist-superlink-state.rst @@ -0,0 +1,39 @@ +Persist the State of the SuperLink +================================== + +By default, the Flower SuperLink keeps its state in-memory. When using the Docker flag ``--rm``, the +state is not persisted between container starts. + +If you want to persist the state of the SuperLink on your host system, all you need to do is specify +a directory where you want to save the file on your host system and a name for the database file. + +By default, the SuperLink container runs with a non-root user called ``app`` with the user ID +``49999``. It is recommended to create a new directory and change the user ID of the directory to +``49999`` to ensure the mounted directory has the proper permissions. + +If you later want to delete the directory, you can change the user ID back to the current user +ID by running ``sudo chown -R $USER:$(id -gn) state``. + +Example +------- + +In the example below, we create a new directory called ``state``, change the user ID and tell +Docker via the flag ``--volume`` to mount the local ``state`` directory into the ``/app/state`` +directory of the container. Lastly, we use the flag ``--database`` to specify the name of the +database file. + +.. code-block:: bash + :substitutions: + + $ mkdir state + $ sudo chown -R 49999:49999 state + $ docker run --rm \ + --volume ./state/:/app/state flwr/superlink:|stable_flwr_version| \ + --database state.db \ + ... + +As soon as the SuperLink starts, the file ``state.db`` is created in the ``state`` directory on +your host system. If the file already exists, the SuperLink tries to restore the state from the +file. To start the SuperLink with an empty database, ensure that there is no database +called ``state.db`` in the ``state`` directory (``rm state.db``) before you execute the +``docker run`` command above. diff --git a/doc/source/docker/pin-version.rst b/doc/source/docker/pin-version.rst new file mode 100644 index 000000000000..800e3ed95423 --- /dev/null +++ b/doc/source/docker/pin-version.rst @@ -0,0 +1,36 @@ +Pin a Docker Image to a Specific Version +======================================== + +It may happen that we update the images behind the tags. Such updates usually include security +updates of system dependencies that should not change the functionality of Flower. However, if +you want to ensure that you use a fixed version of the Docker image in your deployments, you can +`specify the digest `_ +of the image instead of the tag. + +Example +------- + +The following command returns the current image digest referenced by the +:substitution-code:`superlink:|stable_flwr_version|` tag: + +.. code-block:: bash + :substitutions: + + $ docker pull flwr/superlink:|stable_flwr_version| + $ docker inspect --format='{{index .RepoDigests 0}}' flwr/superlink:|stable_flwr_version| + +This will output + +.. code-block:: bash + :substitutions: + + flwr/superlink@sha256:|stable__flwr_superlink_docker_digest| + +Next, we can pin the digest when running a new SuperLink container: + +.. code-block:: bash + :substitutions: + + $ docker run \ + --rm flwr/superlink@sha256:|latest_version_docker_sha| \ + [OPTIONS] diff --git a/doc/source/docker/run-as-root-user.rst b/doc/source/docker/run-as-root-user.rst new file mode 100644 index 000000000000..d1b41a9b6168 --- /dev/null +++ b/doc/source/docker/run-as-root-user.rst @@ -0,0 +1,45 @@ +Run with Root User Privileges +============================= + +Flower Docker images, by default, run with a non-root user (username/groupname: ``app``, +UID/GID: ``49999``). Using root user is **not recommended** unless it is necessary for specific +tasks during the build process. + +Always make sure to run the container as a non-root user in production to maintain security +best practices. + +Run a Container with Root User Privileges +----------------------------------------- + +Run the Docker image with the ``-u`` flag and specify ``root`` as the username: + +.. code-block:: bash + :substitutions: + + $ docker run --rm -u root flwr/superlink:|stable_flwr_version| + +This command will run the Docker container with root user privileges. + +Run the Build Process with Root User Privileges +----------------------------------------------- + +If you want to switch to the root user during the build process of the Docker image to install +missing system dependencies, you can use the ``USER root`` directive within your Dockerfile. + +.. code-block:: dockerfile + :caption: SuperNode Dockerfile + :substitutions: + + FROM flwr/supernode:|stable_flwr_version| + + # Switch to root user + USER root + + # Install missing dependencies (requires root access) + RUN apt-get update && apt-get install -y + + # Switch back to non-root user app + USER app + + # Continue with your Docker image build process + # ... diff --git a/doc/source/docker/set-environment-variables.rst b/doc/source/docker/set-environment-variables.rst new file mode 100644 index 000000000000..ff8d6dde0a29 --- /dev/null +++ b/doc/source/docker/set-environment-variables.rst @@ -0,0 +1,14 @@ +Set Environment Variables +========================= + +To set a variable inside a Docker container, you can use the ``-e =`` flag. +Multiple ``-e`` flags can be used to set multiple environment variables for a container. + +Example +------- + +.. code-block:: bash + :substitutions: + + $ docker run -e FLWR_TELEMETRY_ENABLED=0 -e FLWR_TELEMETRY_LOGGING=0 \ + --rm flwr/superlink:|stable_flwr_version| diff --git a/doc/source/docker/tutorial-quickstart-docker.rst b/doc/source/docker/tutorial-quickstart-docker.rst new file mode 100644 index 000000000000..13f15bc4fb72 --- /dev/null +++ b/doc/source/docker/tutorial-quickstart-docker.rst @@ -0,0 +1,369 @@ +Quickstart with Docker +====================== + +This quickstart aims to guide you through the process of containerizing a Flower project and +running it end to end using Docker on your local machine. + +This tutorial does not use production-ready settings, so you can focus on understanding the basic +workflow that uses the minimum configurations. + +Prerequisites +------------- + +Before you start, make sure that: + +- The ``flwr`` CLI is :doc:`installed <../how-to-install-flower>` locally. +- The Docker daemon is running. + +Step 1: Set Up +-------------- + +#. Create a new Flower project (PyTorch): + + .. code-block:: bash + + $ flwr new quickstart-docker --framework PyTorch --username flower + + πŸ”¨ Creating Flower project quickstart-docker... + 🎊 Project creation successful. + + Use the following command to run your project: + + cd quickstart-docker + pip install -e . + flwr run + + $ cd quickstart-docker + $ pip install -e . + +#. Create a new Docker bridge network called ``flwr-network``: + + .. code-block:: bash + + $ docker network create --driver bridge flwr-network + + User-defined networks, such as ``flwr-network``, enable IP resolution of container names, a feature + absent in the default bridge network. This simplifies quickstart example by avoiding the need to + determine host IP first. + +Step 2: Start the SuperLink +--------------------------- + +Open your terminal and run: + +.. code-block:: bash + :substitutions: + + $ docker run --rm \ + -p 9091:9091 -p 9092:9092 \ + --network flwr-network \ + --name superlink \ + --detach \ + flwr/superlink:|stable_flwr_version| --insecure + +.. dropdown:: Understand the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``-p 9091:9091 -p 9092:9092``: Map port ``9091`` and ``9092`` of the container to the same port of + | the host machine, allowing you to access the Driver API on ``http://localhost:9091`` and + | the Fleet API on ``http://localhost:9092``. + * ``--network flwr-network``: Make the container join the network named ``flwr-network``. + * ``--name superlink``: Assign the name ``superlink`` to the container. + * ``--detach``: Run the container in the background, freeing up the terminal. + * | :substitution-code:`flwr/superlink:|stable_flwr_version|`: The name of the image to be run and the specific + | tag of the image. The tag :substitution-code:`|stable_flwr_version|` represents a :doc:`specific version ` of the image. + * | ``--insecure``: This flag tells the container to operate in an insecure mode, allowing + | unencrypted communication. + +Step 3: Start the SuperNode +--------------------------- + +The SuperNode Docker image comes with a pre-installed version of Flower and serves as a base for +building your own SuperNode image. + +#. Create a SuperNode Dockerfile called ``Dockerfile.supernode`` and paste the following code into it: + + .. code-block:: dockerfile + :caption: Dockerfile.supernode + :linenos: + :substitutions: + + FROM flwr/supernode:|stable_flwr_version| + + WORKDIR /app + COPY pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + COPY flower.quickstart-docker.1-0-0.fab . + RUN flwr install flower.quickstart-docker.1-0-0.fab + + ENTRYPOINT ["flower-supernode"] + + .. dropdown:: Understand the Dockerfile + + * | :substitution-code:`FROM flwr/supernode:|stable_flwr_version|`: This line specifies that the Docker image + | to be built from is the ``flwr/supernode image``, version :substitution-code:`|stable_flwr_version|`. + * | ``WORKDIR /app``: Set the working directory for the container to ``/app``. + | Any subsequent commands that reference a directory will be relative to this directory. + * | ``COPY pyproject.toml .``: Copy the ``pyproject.toml`` file + | from the current working directory into the container's ``/app`` directory. + * | ``RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml``: Remove the ``flwr`` dependency + | from the ``pyproject.toml``. + * | ``python -m pip install -U --no-cache-dir .``: Run the ``pip`` install command to + | install the dependencies defined in the ``pyproject.toml`` file + | + | The ``-U`` flag indicates that any existing packages should be upgraded, and + | ``--no-cache-dir`` prevents pip from using the cache to speed up the installation. + * | ``COPY flower.quickstart-docker.1-0-0.fab .``: Copy the + | ``flower.quickstart-docker.1-0-0.fab`` file from the current working directory into + | the container's ``/app`` directory. + * | ``RUN flwr install flower.quickstart-docker.1-0-0.fab``: Run the ``flwr`` install command + | to install the Flower App Bundle locally. + * | ``ENTRYPOINT ["flower-supernode"]``: Set the command ``flower-supernode`` to be + | the default command run when the container is started. + + .. important:: + + Note that `flwr `__ is already installed in the ``flwr/supernode`` + base image, so only other package dependencies such as ``flwr-datasets``, ``torch``, etc., + need to be installed. As a result, the ``flwr`` dependency is removed from the + ``pyproject.toml`` after it has been copied into the Docker image (see line 5). + +#. Build the Flower App Bundle (FAB): + + .. code-block:: bash + + $ flwr build + +#. Next, build the SuperNode Docker image by running the following command in the directory where + Dockerfile is located: + + .. code-block:: bash + + $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . + + .. note:: + + The image name was set as ``flwr_supernode`` with the tag ``0.0.1``. Remember that + these values are merely examples, and you can customize them according to your requirements. + +#. Start the first SuperNode container: + + .. code-block:: bash + + $ docker run --rm \ + --network flwr-network \ + --detach \ + flwr_supernode:0.0.1 \ + --insecure \ + --superlink superlink:9092 \ + --node-config \ + partition-id=0,num-partitions=2 + + .. dropdown:: Understand the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * ``--network flwr-network``: Make the container join the network named ``flwr-network``. + * ``--detach``: Run the container in the background, freeing up the terminal. + * | ``flwr_supernode:0.0.1``: This is the name of the image to be run and the specific tag + | of the image. + * | ``--insecure``: This flag tells the container to operate in an insecure mode, allowing + | unencrypted communication. + * | ``--superlink superlink:9092``: Connect to the SuperLinks Fleet API on the address + | ``superlink:9092``. + * | ``--node-config partition-id=0,num-partitions=2``: Set the partition ID to ``0`` and the + | number of partitions to ``2`` for the SuperNode configuration. + +#. Start the second SuperNode container: + + .. code-block:: shell + + $ docker run --rm \ + --network flwr-network \ + --detach \ + flwr_supernode:0.0.1 \ + --insecure \ + --superlink superlink:9092 \ + --node-config \ + partition-id=1,num-partitions=2 + +Step 4: Start the SuperExec +--------------------------- + +The procedure for building and running a SuperExec image is almost identical to the SuperNode image. + +Similar to the SuperNode image, the SuperExec Docker image comes with a pre-installed version of +Flower and serves as a base for building your own SuperExec image. + +#. Create a SuperExec Dockerfile called ``Dockerfile.superexec`` and paste the following code in: + + .. code-block:: dockerfile + :caption: Dockerfile.superexec + :substitutions: + + FROM flwr/superexec:|stable_flwr_version| + + WORKDIR /app + + COPY pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-superexec", "--executor", "flwr.superexec.deployment:executor"] + + .. dropdown:: Understand the Dockerfile + + * | :substitution-code:`FROM flwr/superexec:|stable_flwr_version|`: This line specifies that the Docker image + | to be built from is the ``flwr/superexec image``, version :substitution-code:`|stable_flwr_version|`. + * | ``WORKDIR /app``: Set the working directory for the container to ``/app``. + | Any subsequent commands that reference a directory will be relative to this directory. + * | ``COPY pyproject.toml .``: Copy the ``pyproject.toml`` file + | from the current working directory into the container's ``/app`` directory. + * | ``RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml``: Remove the ``flwr`` dependency + | from the ``pyproject.toml``. + * | ``python -m pip install -U --no-cache-dir .``: Run the ``pip`` install command to + | install the dependencies defined in the ``pyproject.toml`` file + | + | The ``-U`` flag indicates that any existing packages should be upgraded, and + | ``--no-cache-dir`` prevents pip from using the cache to speed up the installation. + * | ``ENTRYPOINT ["flower-superexec"``: Set the command ``flower-superexec`` to be + | the default command run when the container is started. + | + | ``"--executor", "flwr.superexec.deployment:executor"]`` Use the + | ``flwr.superexec.deployment:executor`` executor to run the ServerApps. + +#. Afterward, in the directory that holds the Dockerfile, execute this Docker command to + build the SuperExec image: + + .. code-block:: bash + + $ docker build -f Dockerfile.superexec -t flwr_superexec:0.0.1 . + + +#. Start the SuperExec container: + + .. code-block:: bash + + $ docker run --rm \ + -p 9093:9093 \ + --network flwr-network \ + --name superexec \ + --detach \ + flwr_superexec:0.0.1 \ + --insecure \ + --executor-config \ + superlink=\"superlink:9091\" + + .. dropdown:: Understand the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``-p 9093:9093``: Map port ``9093`` of the container to the same port of + | the host machine, allowing you to access the SuperExec API on ``http://localhost:9093``. + * ``--network flwr-network``: Make the container join the network named ``flwr-network``. + * ``--name superexec``: Assign the name ``superexec`` to the container. + * ``--detach``: Run the container in the background, freeing up the terminal. + * | ``flwr_superexec:0.0.1``: This is the name of the image to be run and the specific tag + | of the image. + * | ``--insecure``: This flag tells the container to operate in an insecure mode, allowing + | unencrypted communication. + * | ``--executor-config superlink=\"superlink:9091\"``: Configure the SuperExec executor to + | connect to the SuperLink running on port ``9091``. + +Step 5: Run the Quickstart Project +---------------------------------- + +#. Add the following lines to the ``pyproject.toml``: + + .. code-block:: toml + :caption: pyproject.toml + + [tool.flwr.federations.docker] + address = "127.0.0.1:9093" + insecure = true + +#. Run the ``quickstart-docker`` project by executing the command: + + .. code-block:: bash + + $ flwr run . docker + +#. Follow the SuperExec logs to track the execution of the run: + + .. code-block:: bash + + $ docker logs -f superexec + +Step 6: Update the Application +------------------------------ + +#. Change the application code. For example, change the ``seed`` in ``quickstart_docker/task.py`` + to ``43`` and save it: + + .. code-block:: python + :caption: quickstart_docker/task.py + + # ... + partition_train_test = partition.train_test_split(test_size=0.2, seed=43) + # ... + +#. Stop the current SuperNode containers: + + .. code-block:: bash + + $ docker stop $(docker ps -a -q --filter ancestor=flwr_supernode:0.0.1) + +#. Rebuild the FAB and SuperNode image: + + .. code-block:: bash + + $ flwr build + $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . + +#. Launch two new SuperNode containers based on the newly built image: + + .. code-block:: bash + + $ docker run --rm \ + --network flwr-network \ + --detach \ + flwr_supernode:0.0.1 \ + --insecure \ + --superlink superlink:9092 \ + --node-config \ + partition-id=0,num-partitions=2 + $ docker run --rm \ + --network flwr-network \ + --detach \ + flwr_supernode:0.0.1 \ + --insecure \ + --superlink superlink:9092 \ + --node-config \ + partition-id=1,num-partitions=2 + +#. Run the updated project: + + .. code-block:: bash + + $ flwr run . docker + +Step 7: Clean Up +---------------- + +Remove the containers and the bridge network: + +.. code-block:: bash + + $ docker stop $(docker ps -a -q --filter ancestor=flwr_supernode:0.0.1) \ + superexec \ + superlink + $ docker network rm flwr-network + +Where to Go Next +---------------- + +* :doc:`enable-tls` +* :doc:`persist-superlink-state` diff --git a/doc/source/docker/use-a-different-version.rst b/doc/source/docker/use-a-different-version.rst new file mode 100644 index 000000000000..73e5f4218663 --- /dev/null +++ b/doc/source/docker/use-a-different-version.rst @@ -0,0 +1,12 @@ +Use a Different Flower Version +============================== + +If you want to use a different version of Flower, for example Flower nightly, you can do so by +changing the tag. All available versions are on `Docker Hub `__. + +.. important:: + + When using Flower nightly, the SuperLink nightly image must be paired with the corresponding + SuperNode and ServerApp nightly images released on the same day. To ensure the versions are + in sync, using the concrete tag, e.g., ``1.10.0.dev20240610`` instead of ``nightly`` is + recommended. diff --git a/doc/source/how-to-install-flower.rst b/doc/source/how-to-install-flower.rst index 10f0f221b4c8..d773e6999245 100644 --- a/doc/source/how-to-install-flower.rst +++ b/doc/source/how-to-install-flower.rst @@ -60,7 +60,7 @@ Advanced installation options Install via Docker ~~~~~~~~~~~~~~~~~~ -:doc:`How to run Flower using Docker ` +:doc:`Run Flower using Docker ` Install pre-release ~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/how-to-run-flower-using-docker.rst b/doc/source/how-to-run-flower-using-docker.rst deleted file mode 100644 index 6272c5c131c3..000000000000 --- a/doc/source/how-to-run-flower-using-docker.rst +++ /dev/null @@ -1,501 +0,0 @@ -Run Flower using Docker -======================= - -The simplest way to get started with Flower is by using the pre-made Docker images, which you can -find on `Docker Hub `__. Supported architectures include ``amd64`` -and ``arm64v8``. - -Before you start, make sure that the Docker daemon is running: - -.. code-block:: bash - - $ docker -v - Docker version 26.0.0, build 2ae903e - -If you do not see the version of Docker but instead get an error saying that the command -was not found, you will need to install Docker first. You can find installation instruction -`here `_. - -.. note:: - - On Linux, Docker commands require ``sudo`` privilege. If you want to avoid using ``sudo``, - you can follow the `Post-installation steps `_ - on the official Docker website. - -.. important:: - - To ensure optimal performance and compatibility, the SuperLink, SuperNode and ServerApp image - must have the same version when running together. This guarantees seamless integration and - avoids potential conflicts or issues that may arise from using different versions. - -Flower SuperLink ----------------- - -Quickstart -~~~~~~~~~~ - -If you're looking to try out Flower, you can use the following command: - -.. code-block:: bash - :substitutions: - - $ docker run --rm -p 9091:9091 -p 9092:9092 flwr/superlink:|stable_flwr_version| --insecure - -The command pulls the Docker image with the tag :substitution-code:`|stable_flwr_version|` from Docker Hub. The tag specifies -the Flower version. In this case, Flower |stable_flwr_version|. The ``--rm`` flag tells Docker to remove the -container after it exits. - -.. note:: - - By default, the Flower SuperLink keeps state in-memory. When using the Docker flag ``--rm``, the - state is not persisted between container starts. We will show below how to save the state in a - file on your host system. - -The ``-p :`` flag tells Docker to map the ports ``9091``/``9092`` of the host to -``9091``/``9092`` of the container, allowing you to access the Driver API on ``http://localhost:9091`` -and the Fleet API on ``http://localhost:9092``. Lastly, any flag that comes after the tag is passed -to the Flower SuperLink. Here, we are passing the flag ``--insecure``. - -.. attention:: - - The ``--insecure`` flag enables insecure communication (using HTTP, not HTTPS) and should only be - used for testing purposes. We strongly recommend enabling - `SSL `__ - when deploying to a production environment. - -You can use ``--help`` to view all available flags that the SuperLink supports: - -.. code-block:: bash - :substitutions: - - $ docker run --rm flwr/superlink:|stable_flwr_version| --help - -Mounting a volume to store the state on the host system -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to persist the state of the SuperLink on your host system, all you need to do is specify -a directory where you want to save the file on your host system and a name for the database file. By -default, the SuperLink container runs with a non-root user called ``app`` with the user ID -``49999``. It is recommended to create new directory and change the user ID of the directory to -``49999`` to ensure the mounted directory has the proper permissions. If you later want to delete -the directory, you can change the user ID back to the current user ID by running -``sudo chown -R $USER:$(id -gn) state``. - -In the example below, we create a new directory, change the user ID and tell Docker via the flag -``--volume`` to mount the local ``state`` directory into the ``/app/state`` directory of the -container. Furthermore, we use the flag ``--database`` to specify the name of the database file. - -.. code-block:: bash - :substitutions: - - $ mkdir state - $ sudo chown -R 49999:49999 state - $ docker run --rm \ - -p 9091:9091 -p 9092:9092 --volume ./state/:/app/state flwr/superlink:|stable_flwr_version| \ - --insecure \ - --database state.db - -As soon as the SuperLink starts, the file ``state.db`` is created in the ``state`` directory on -your host system. If the file already exists, the SuperLink tries to restore the state from the -file. To start the SuperLink with an empty database, simply remove the ``state.db`` file. - -Enabling SSL for secure connections -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To enable SSL, you will need a PEM-encoded root certificate, a PEM-encoded private key and a -PEM-encoded certificate chain. - -.. note:: - For testing purposes, you can generate your own self-signed certificates. The - `Enable SSL connections `__ - page contains a section that will guide you through the process. - -Assuming all files we need are in the local ``certificates`` directory, we can use the flag -``--volume`` to mount the local directory into the ``/app/certificates/`` directory of the container. -This allows the SuperLink to access the files within the container. The ``ro`` stands for -``read-only``. Docker volumes default to ``read-write``; that option tells Docker to make the volume -``read-only`` instead. Finally, we pass the names of the certificates and key file to the SuperLink -with the ``--ssl-ca-certfile``, ``--ssl-certfile`` and ``--ssl-keyfile`` flag. - -.. code-block:: bash - :substitutions: - - $ docker run --rm \ - -p 9091:9091 -p 9092:9092 \ - --volume ./certificates/:/app/certificates/:ro flwr/superlink:|stable_flwr_version| \ - --ssl-ca-certfile certificates/ca.crt \ - --ssl-certfile certificates/server.pem \ - --ssl-keyfile certificates/server.key - -.. note:: - - Because Flower containers, by default, run with a non-root user ``app``, the mounted files and - directories must have the proper permissions for the user ID ``49999``. For example, to change the - user ID of all files in the ``certificates/`` directory, you can run - ``sudo chown -R 49999:49999 certificates/*``. - -Flower SuperNode ----------------- - -The SuperNode Docker image comes with a pre-installed version of Flower and serves as a base for -building your own SuperNode image. - -We will use the ``quickstart-pytorch`` example, which you can find in -the Flower repository, to illustrate how you can dockerize your ClientApp. - -.. _SuperNode Prerequisites: - -Prerequisites -~~~~~~~~~~~~~ - -Before we can start, we need to meet a few prerequisites in our local development environment. -You can skip the first part if you want to run your ClientApp instead of the ``quickstart-pytorch`` -example. - -#. Clone the Flower repository. - - .. code-block:: bash - - $ git clone --depth=1 https://github.com/adap/flower.git && cd flower/examples/quickstart-pytorch - -#. Verify the Docker daemon is running. - - Please follow the first section on - :doc:`Run Flower using Docker ` - which covers this step in more detail. - - -Creating a SuperNode Dockerfile -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Let's assume the following project layout: - -.. code-block:: bash - - $ tree . - . - β”œβ”€β”€ client.py # ClientApp code - └── - -First, we need to create a ``requirements.txt`` file in the directory where the ``ClientApp`` code -is located. In the file, we list all the dependencies that the ClientApp requires. - -.. code-block:: - - flwr-datasets[vision]>=0.1.0,<1.0.0 - torch==2.2.1 - torchvision==0.17.1 - tqdm==4.66.3 - -.. important:: - - Note that `flwr `__ is already installed in the ``flwr/supernode`` - base image, so you only need to include other package dependencies in your ``requirements.txt``, - such as ``torch``, ``tensorflow``, etc. - -Next, we create a Dockerfile. If you use the ``quickstart-pytorch`` example, create a new -file called ``Dockerfile.supernode`` in ``examples/quickstart-pytorch``. - -The ``Dockerfile.supernode`` contains the instructions that assemble the SuperNode image. - -.. code-block:: dockerfile - :substitutions: - - FROM flwr/supernode:|stable_flwr_version| - - WORKDIR /app - - COPY requirements.txt . - RUN python -m pip install -U --no-cache-dir -r requirements.txt - - COPY client.py ./ - ENTRYPOINT ["flower-client-app", "client:app"] - -In the first two lines, we instruct Docker to use the SuperNode image tagged :substitution-code:`|stable_flwr_version|` as a base -image and set our working directory to ``/app``. The following instructions will now be -executed in the ``/app`` directory. Next, we install the ClientApp dependencies by copying the -``requirements.txt`` file into the image and run ``pip install``. In the last two lines, -we copy the ``client.py`` module into the image and set the entry point to ``flower-client-app`` with -the argument ``client:app``. The argument is the object reference of the ClientApp -(``:``) that will be run inside the ClientApp. - -Building the SuperNode Docker image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, we build the SuperNode Docker image by running the following command in the directory where -Dockerfile and ClientApp code are located. - -.. code-block:: bash - - $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . - -We gave the image the name ``flwr_supernode``, and the tag ``0.0.1``. Remember that the here chosen -values only serve as an example. You can change them to your needs. - - -Running the SuperNode Docker image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that we have built the SuperNode image, we can finally run it. - -.. code-block:: bash - - $ docker run --rm flwr_supernode:0.0.1 \ - --insecure \ - --superlink 192.168.1.100:9092 - -Let's break down each part of this command: - -* ``docker run``: This is the command to run a new Docker container. -* ``--rm``: This option specifies that the container should be automatically removed when it stops. -* ``flwr_supernode:0.0.1``: The name the tag of the Docker image to use. -* ``--insecure``: This option enables insecure communication. - -.. attention:: - - The ``--insecure`` flag enables insecure communication (using HTTP, not HTTPS) and should only be - used for testing purposes. We strongly recommend enabling - `SSL `__ - when deploying to a production environment. - -* | ``--superlink 192.168.1.100:9092``: This option specifies the address of the SuperLinks Fleet - | API to connect to. Remember to update it with your SuperLink IP. - -.. note:: - - To test running Flower locally, you can create a - `bridge network `__, - use the ``--network`` argument and pass the name of the Docker network to run your SuperNodes. - -Any argument that comes after the tag is passed to the Flower SuperNode binary. -To see all available flags that the SuperNode supports, run: - -.. code-block:: bash - :substitutions: - - $ docker run --rm flwr/supernode:|stable_flwr_version| --help - -Enabling SSL for secure connections -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To enable SSL, we will need to mount a PEM-encoded root certificate into your SuperNode container. - -Assuming the certificate already exists locally, we can use the flag ``--volume`` to mount the local -certificate into the container's ``/app/`` directory. This allows the SuperNode to access the -certificate within the container. Use the ``--root-certificates`` flag when starting the container. - -.. code-block:: bash - - - $ docker run --rm --volume ./ca.crt:/app/ca.crt flwr_supernode:0.0.1 \ - --superlink 192.168.1.100:9092 \ - --root-certificates ca.crt - -Flower ServerApp ----------------- - -The procedure for building and running a ServerApp image is almost identical to the SuperNode image. - -Similar to the SuperNode image, the ServerApp Docker image comes with a pre-installed version of -Flower and serves as a base for building your own ServerApp image. - -We will use the same ``quickstart-pytorch`` example as we do in the Flower SuperNode section. -If you have not already done so, please follow the `SuperNode Prerequisites`_ before proceeding. - - -Creating a ServerApp Dockerfile -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Let's assume the following project layout: - -.. code-block:: bash - - $ tree . - . - β”œβ”€β”€ server.py # ServerApp code - └── - -First, we need to create a Dockerfile in the directory where the ``ServerApp`` code is located. -If you use the ``quickstart-pytorch`` example, create a new file called ``Dockerfile.serverapp`` in -``examples/quickstart-pytorch``. - -The ``Dockerfile.serverapp`` contains the instructions that assemble the ServerApp image. - -.. code-block:: dockerfile - :substitutions: - - FROM flwr/serverapp:|stable_flwr_version| - - WORKDIR /app - - COPY server.py ./ - ENTRYPOINT ["flower-server-app", "server:app"] - -In the first two lines, we instruct Docker to use the ServerApp image tagged :substitution-code:`|stable_flwr_version|` as a base -image and set our working directory to ``/app``. The following instructions will now be -executed in the ``/app`` directory. In the last two lines, we copy the ``server.py`` module into the -image and set the entry point to ``flower-server-app`` with the argument ``server:app``. -The argument is the object reference of the ServerApp (``:``) that will be run -inside the ServerApp container. - -Building the ServerApp Docker image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, we build the ServerApp Docker image by running the following command in the directory where -Dockerfile and ServerApp code are located. - -.. code-block:: bash - - $ docker build -f Dockerfile.serverapp -t flwr_serverapp:0.0.1 . - -We gave the image the name ``flwr_serverapp``, and the tag ``0.0.1``. Remember that the here chosen -values only serve as an example. You can change them to your needs. - - -Running the ServerApp Docker image -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Now that we have built the ServerApp image, we can finally run it. - -.. code-block:: bash - - $ docker run --rm flwr_serverapp:0.0.1 \ - --insecure \ - --superlink 192.168.1.100:9091 - -Let's break down each part of this command: - -* ``docker run``: This is the command to run a new Docker container. -* ``--rm``: This option specifies that the container should be automatically removed when it stops. -* ``flwr_serverapp:0.0.1``: The name the tag of the Docker image to use. -* ``--insecure``: This option enables insecure communication. - -.. attention:: - - The ``--insecure`` flag enables insecure communication (using HTTP, not HTTPS) and should only be - used for testing purposes. We strongly recommend enabling - `SSL `__ - when deploying to a production environment. - -* | ``--superlink 192.168.1.100:9091``: This option specifies the address of the SuperLinks Driver - | API to connect to. Remember to update it with your SuperLink IP. - -.. note:: - To test running Flower locally, you can create a - `bridge network `__, - use the ``--network`` argument and pass the name of the Docker network to run your ServerApps. - -Any argument that comes after the tag is passed to the Flower ServerApp binary. -To see all available flags that the ServerApp supports, run: - -.. code-block:: bash - :substitutions: - - $ docker run --rm flwr/serverapp:|stable_flwr_version| --help - -Enabling SSL for secure connections -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To enable SSL, we will need to mount a PEM-encoded root certificate into your ServerApp container. - -Assuming the certificate already exists locally, we can use the flag ``--volume`` to mount the local -certificate into the container's ``/app/`` directory. This allows the ServerApp to access the -certificate within the container. Use the ``--root-certificates`` flags when starting the container. - -.. code-block:: bash - - $ docker run --rm --volume ./ca.crt:/app/ca.crt flwr_serverapp:0.0.1 \ - --superlink 192.168.1.100:9091 \ - --root-certificates ca.crt - -Advanced Docker options ------------------------ - -Run with root user privileges -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Flower Docker images, by default, run with a non-root user (username/groupname: ``app``, -UID/GID: ``49999``). Using root user is not recommended unless it is necessary for specific -tasks during the build process. Always make sure to run the container as a non-root user in -production to maintain security best practices. - -**Run a container with root user privileges** - -Run the Docker image with the ``-u`` flag and specify ``root`` as the username: - -.. code-block:: bash - :substitutions: - - $ docker run --rm -u root flwr/superlink:|stable_flwr_version| - -This command will run the Docker container with root user privileges. - -**Run the build process with root user privileges** - -If you want to switch to the root user during the build process of the Docker image to install -missing system dependencies, you can use the ``USER root`` directive within your Dockerfile. - -.. code-block:: dockerfile - :substitutions: - - FROM flwr/supernode:|stable_flwr_version| - - # Switch to root user - USER root - - # Install missing dependencies (requires root access) - RUN apt-get update && apt-get install -y - - # Switch back to non-root user app - USER app - - # Continue with your Docker image build process - ... - -Using a different Flower version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to use a different version of Flower, for example Flower nightly, you can do so by -changing the tag. All available versions are on `Docker Hub `__. - -.. important:: - - When using Flower nightly, the SuperLink nightly image must be paired with the corresponding - SuperNode and ServerApp nightly images released on the same day. To ensure the versions are - in sync, using the concrete tag, e.g., ``1.10.0.dev20240610`` instead of ``nightly`` is - recommended. - -Pinning a Docker image to a specific version -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It may happen that we update the images behind the tags. Such updates usually include security -updates of system dependencies that should not change the functionality of Flower. However, if you -want to ensure that you always use the same image, you can specify the hash of the image instead of -the tag. - -The following command returns the current image hash referenced by the :substitution-code:`superlink:|stable_flwr_version|` tag: - -.. code-block:: bash - :substitutions: - - $ docker inspect --format='{{index .RepoDigests 0}}' flwr/superlink:|stable_flwr_version| - flwr/superlink@sha256:985c24b2b337ab7f15a554fde9d860cede95079bcaa244fda8f12c0805e34c7d - -Next, we can pin the hash when running a new SuperLink container: - -.. code-block:: bash - - $ docker run \ - --rm flwr/superlink@sha256:985c24b2b337ab7f15a554fde9d860cede95079bcaa244fda8f12c0805e34c7d \ - --insecure - -Setting environment variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To set a variable inside a Docker container, you can use the ``-e =`` flag. - -.. code-block:: bash - :substitutions: - - $ docker run -e FLWR_TELEMETRY_ENABLED=0 \ - --rm flwr/superlink:|stable_flwr_version| --insecure diff --git a/doc/source/index.rst b/doc/source/index.rst index 399f27d49596..2a34693f7b26 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -93,7 +93,7 @@ Problem-oriented how-to guides show step-by-step how to achieve a specific goal. how-to-use-built-in-mods how-to-use-differential-privacy how-to-authenticate-supernodes - how-to-run-flower-using-docker + docker/index how-to-upgrade-to-flower-1.0 how-to-upgrade-to-flower-next From 97a991f89abde03c1d0f417db9aee5e27ce4f5c6 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:47:42 +0200 Subject: [PATCH 104/188] feat(datasets) Add dataset type check for dataset assignment (#4058) --- .../flwr_datasets/partitioner/partitioner.py | 5 ++ .../partitioner/partitioner_test.py | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 datasets/flwr_datasets/partitioner/partitioner_test.py diff --git a/datasets/flwr_datasets/partitioner/partitioner.py b/datasets/flwr_datasets/partitioner/partitioner.py index 24ca22bbebcb..0404a11e772c 100644 --- a/datasets/flwr_datasets/partitioner/partitioner.py +++ b/datasets/flwr_datasets/partitioner/partitioner.py @@ -50,6 +50,11 @@ def dataset(self, value: Dataset) -> None: "created partitions (in case the partitioning scheme needs to create " "the full partitioning also in order to return a single partition)." ) + if not isinstance(value, Dataset): + raise TypeError( + f"The dataset object you want to assign to the partitioner should be " + f"of type `datasets.Dataset` but given {type(value)}." + ) self._dataset = value @abstractmethod diff --git a/datasets/flwr_datasets/partitioner/partitioner_test.py b/datasets/flwr_datasets/partitioner/partitioner_test.py new file mode 100644 index 000000000000..be0c988e6a9a --- /dev/null +++ b/datasets/flwr_datasets/partitioner/partitioner_test.py @@ -0,0 +1,59 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Abstract partitioner tests.""" + + +import unittest + +import datasets +from datasets import Dataset +from flwr_datasets.partitioner.partitioner import Partitioner + + +class DummyPartitioner(Partitioner): + """Dummy partitioner for testing.""" + + def load_partition(self, partition_id: int) -> Dataset: + """Return always a dummy dataset.""" + return datasets.Dataset.from_dict({"feature": [0, 1, 2]}) + + @property + def num_partitions(self) -> int: + """Return always 0.""" + return 0 + + +class TestPartitioner(unittest.TestCase): + """Test Partitioner.""" + + def test_dataset_setter_incorrect_type(self) -> None: + """Test if the incorrect type of the dataset to dataset.setter method raises.""" + train_split = datasets.Dataset.from_dict({"feature": [0, 1, 2]}) + test_split = datasets.Dataset.from_dict({"feature": [0, 1, 2]}) + dataset = datasets.DatasetDict({"train": train_split, "test": test_split}) + partitioner = DummyPartitioner() + + with self.assertRaises(Exception) as context: + partitioner.dataset = dataset + self.assertIn( + "The dataset object you want to assign to the partitioner should be of " + "type `datasets.Dataset` but given " + ".", + str(context.exception), + ) + + +if __name__ == "__main__": + unittest.main() From 2689ee5bfacc2f1f5e3cb2c04378fdbed40a4f20 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:20:14 +0200 Subject: [PATCH 105/188] docs(datasets) Update information how to handle DatasetDict local data (#4057) --- .../doc/source/how-to-use-with-local-data.rst | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/datasets/doc/source/how-to-use-with-local-data.rst b/datasets/doc/source/how-to-use-with-local-data.rst index 276f6d6936ee..3a44ed4f6f38 100644 --- a/datasets/doc/source/how-to-use-with-local-data.rst +++ b/datasets/doc/source/how-to-use-with-local-data.rst @@ -37,14 +37,6 @@ CSV data_files = [ "path-to-my-file-1.csv", "path-to-my-file-2.csv", ...] dataset = load_dataset("csv", data_files=data_files) - # Divided Dataset - data_files = { - "train": single_train_file_or_list_of_files, - "test": single_test_file_or_list_of_files, - "can-have-more-splits": ... - } - dataset = load_dataset("csv", data_files=data_files) - partitioner = ChosenPartitioner(...) partitioner.dataset = dataset partition = partitioner.load_partition(partition_id=0) @@ -60,18 +52,10 @@ JSON # Single file data_files = "path-to-my-file.json" - # Multitple Files + # Multiple Files data_files = [ "path-to-my-file-1.json", "path-to-my-file-2.json", ...] dataset = load_dataset("json", data_files=data_files) - # Divided Dataset - data_files = { - "train": single_train_file_or_list_of_files, - "test": single_test_file_or_list_of_files, - "can-have-more-splits": ... - } - dataset = load_dataset("json", data_files=data_files) - partitioner = ChosenPartitioner(...) partitioner.dataset = dataset partition = partitioner.load_partition(partition_id=0) @@ -103,7 +87,12 @@ Then, the path you can give is `./mnist`. from flwr_datasets.partitioner import ChosenPartitioner # Directly from a directory - dataset = load_dataset("imagefolder", data_dir="/path/to/folder") + dataset_dict = load_dataset("imagefolder", data_dir="/path/to/folder") + # Note that what we just loaded is a DatasetDict, we need to choose a single split + # and assign it to the partitioner.dataset + # e.g. "train" split but that depends on the structure of your directory + dataset = dataset_dict["train"] + partitioner = ChosenPartitioner(...) partitioner.dataset = dataset partition = partitioner.load_partition(partition_id=0) @@ -134,7 +123,11 @@ Analogously to the image datasets, there are two methods here: from datasets import load_dataset from flwr_datasets.partitioner import ChosenPartitioner - dataset = load_dataset("audiofolder", data_dir="/path/to/folder") + dataset_dict = load_dataset("audiofolder", data_dir="/path/to/folder") + # Note that what we just loaded is a DatasetDict, we need to choose a single split + # and assign it to the partitioner.dataset + # e.g. "train" split but that depends on the structure of your directory + dataset = dataset_dict["train"] partitioner = ChosenPartitioner(...) partitioner.dataset = dataset @@ -230,7 +223,7 @@ Partitioner abstraction is designed to allow for a single dataset assignment. .. code-block:: python - partitioner.dataset = your_dataset + partitioner.dataset = your_dataset # (your_dataset must be of type dataset.Dataset) If you need to do the same partitioning on a different dataset, create a new Partitioner for that, e.g.: From 2019c0e3a1160af7d99145ab2d1b906f6656961e Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 22 Aug 2024 15:54:00 +0100 Subject: [PATCH 106/188] break(framework) Disable `flower-client-app` CLI command (#4022) Co-authored-by: Daniel J. Beutel --- src/py/flwr/client/supernode/app.py | 53 +++-------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 1b027d534a50..8d28e69dea6e 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -16,7 +16,7 @@ import argparse import sys -from logging import DEBUG, INFO, WARN +from logging import DEBUG, ERROR, INFO, WARN from pathlib import Path from typing import Optional, Tuple @@ -90,33 +90,12 @@ def run_supernode() -> None: def run_client_app() -> None: """Run Flower client app.""" - log(INFO, "Long-running Flower client starting") - event(EventType.RUN_CLIENT_APP_ENTER) - - args = _parse_args_run_client_app().parse_args() - - _warn_deprecated_server_arg(args) - - root_certificates = _get_certificates(args) - load_fn = get_load_client_app_fn( - default_app_ref=getattr(args, "client-app"), - app_path=args.dir, - multi_app=False, - ) - authentication_keys = _try_setup_client_authentication(args) - - start_client_internal( - server_address=args.superlink, - node_config=parse_config_args([args.node_config]), - load_client_app_fn=load_fn, - transport=args.transport, - root_certificates=root_certificates, - insecure=args.insecure, - authentication_keys=authentication_keys, - max_retries=args.max_retries, - max_wait_time=args.max_wait_time, + log( + ERROR, + "The command `flower-client-app` has been replaced by `flower-supernode`.", ) + log(INFO, "Execute `flower-supernode --help` to learn how to use it.") register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE) @@ -227,28 +206,6 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser: return parser -def _parse_args_run_client_app() -> argparse.ArgumentParser: - """Parse flower-client-app command line arguments.""" - parser = argparse.ArgumentParser( - description="Start a Flower client app", - ) - - parser.add_argument( - "client-app", - help="For example: `client:app` or `project.package.module:wrapper.app`", - ) - _parse_args_common(parser=parser) - parser.add_argument( - "--dir", - default="", - help="Add specified directory to the PYTHONPATH and load Flower " - "app from there." - " Default: current working directory.", - ) - - return parser - - def _parse_args_common(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--insecure", From 2732bf3f1614347537dcc9401f57f3d363388e24 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 23 Aug 2024 09:23:47 +0100 Subject: [PATCH 107/188] refactor(examples) Update `fl-tabular` example (#4053) Co-authored-by: jafermarq --- examples/fl-tabular/README.md | 47 +++++++++++++++------ examples/fl-tabular/client.py | 39 ----------------- examples/fl-tabular/fltabular/__init__.py | 1 + examples/fl-tabular/fltabular/client_app.py | 43 +++++++++++++++++++ examples/fl-tabular/fltabular/server_app.py | 32 ++++++++++++++ examples/fl-tabular/{ => fltabular}/task.py | 18 +++++++- examples/fl-tabular/pyproject.toml | 26 +++++++++--- examples/fl-tabular/server.py | 25 ----------- 8 files changed, 146 insertions(+), 85 deletions(-) delete mode 100644 examples/fl-tabular/client.py create mode 100644 examples/fl-tabular/fltabular/__init__.py create mode 100644 examples/fl-tabular/fltabular/client_app.py create mode 100644 examples/fl-tabular/fltabular/server_app.py rename examples/fl-tabular/{ => fltabular}/task.py (87%) delete mode 100644 examples/fl-tabular/server.py diff --git a/examples/fl-tabular/README.md b/examples/fl-tabular/README.md index ee6dd7d00ef0..184c8e65fec7 100644 --- a/examples/fl-tabular/README.md +++ b/examples/fl-tabular/README.md @@ -10,9 +10,11 @@ This code exemplifies a federated learning setup using the Flower framework on t This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to download, partition and preprocess the dataset. -## Environments Setup +## Set up the project -Start by cloning the example. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project + +Start by cloning the example project: ```shell git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/fl-tabular . && rm -rf flower && cd fl-tabular @@ -21,25 +23,44 @@ git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/fl- This will create a new directory called `fl-tabular` containing the following files: ```shell --- pyproject.toml --- client.py --- server.py --- task.py --- README.md +fl-tabular +β”œβ”€β”€ fltabular +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing dependencies +### Install dependencies and project -Project dependencies are defined in `pyproject.toml`. Install them with: +Install the dependencies defined in `pyproject.toml` as well as the `fltabular` package. ```shell -pip install . +# From a new python environment, run: +pip install -e . ``` -## Running Code +## Run the Example + +You can run your `ClientApp` and `ServerApp` in both _simulation_ and +_deployment_ mode without making changes to the code. If you are starting +with Flower, we recommend you using the _simulation_ model as it requires +fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. + +### Run with the Simulation Engine + +```bash +flwr run . +``` -### Federated Using Flower Simulation +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flower-simulation --server-app server:app --client-app client:app --num-supernodes 5 +flwr run . --run-config num-server-rounds=10 ``` + +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/fl-tabular/client.py b/examples/fl-tabular/client.py deleted file mode 100644 index 86b78fb21f0c..000000000000 --- a/examples/fl-tabular/client.py +++ /dev/null @@ -1,39 +0,0 @@ -from flwr.client import Client, ClientApp, NumPyClient -from flwr_datasets import FederatedDataset - -from task import IncomeClassifier, evaluate, get_weights, load_data, set_weights, train - -NUMBER_OF_CLIENTS = 5 - - -class FlowerClient(NumPyClient): - def __init__(self, net, trainloader, testloader): - self.net = net - self.trainloader = trainloader - self.testloader = testloader - - def fit(self, parameters, config): - set_weights(self.net, parameters) - train(self.net, self.trainloader) - return get_weights(self.net), len(self.trainloader), {} - - def evaluate(self, parameters, config): - set_weights(self.net, parameters) - loss, accuracy = evaluate(self.net, self.testloader) - return loss, len(self.testloader), {"accuracy": accuracy} - - -def get_client_fn(dataset: FederatedDataset): - def client_fn(cid: str) -> Client: - train_loader, test_loader = load_data(partition_id=int(cid), fds=dataset) - net = IncomeClassifier(14) - return FlowerClient(net, train_loader, test_loader).to_client() - - return client_fn - - -fds = FederatedDataset( - dataset="scikit-learn/adult-census-income", - partitioners={"train": NUMBER_OF_CLIENTS}, -) -app = ClientApp(client_fn=get_client_fn(fds)) diff --git a/examples/fl-tabular/fltabular/__init__.py b/examples/fl-tabular/fltabular/__init__.py new file mode 100644 index 000000000000..075247fb2f4f --- /dev/null +++ b/examples/fl-tabular/fltabular/__init__.py @@ -0,0 +1 @@ +"""fltabular: Flower Example on Adult Census Income Tabular Dataset.""" diff --git a/examples/fl-tabular/fltabular/client_app.py b/examples/fl-tabular/fltabular/client_app.py new file mode 100644 index 000000000000..f3fd0bffb6c0 --- /dev/null +++ b/examples/fl-tabular/fltabular/client_app.py @@ -0,0 +1,43 @@ +"""fltabular: Flower Example on Adult Census Income Tabular Dataset.""" + +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context + +from fltabular.task import ( + IncomeClassifier, + evaluate, + get_weights, + load_data, + set_weights, + train, +) + + +class FlowerClient(NumPyClient): + def __init__(self, net, trainloader, testloader): + self.net = net + self.trainloader = trainloader + self.testloader = testloader + + def fit(self, parameters, config): + set_weights(self.net, parameters) + train(self.net, self.trainloader) + return get_weights(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = evaluate(self.net, self.testloader) + return loss, len(self.testloader), {"accuracy": accuracy} + + +def client_fn(context: Context): + partition_id = context.node_config["partition-id"] + + train_loader, test_loader = load_data( + partition_id=partition_id, num_partitions=context.node_config["num-partitions"] + ) + net = IncomeClassifier() + return FlowerClient(net, train_loader, test_loader).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/fl-tabular/fltabular/server_app.py b/examples/fl-tabular/fltabular/server_app.py new file mode 100644 index 000000000000..7b858700308f --- /dev/null +++ b/examples/fl-tabular/fltabular/server_app.py @@ -0,0 +1,32 @@ +"""fltabular: Flower Example on Adult Census Income Tabular Dataset.""" + +from flwr.common import ndarrays_to_parameters +from flwr.server import ServerApp, ServerConfig, ServerAppComponents +from flwr.server.strategy import FedAvg +from flwr.common import Context + +from fltabular.task import IncomeClassifier, get_weights + + +def weighted_average(metrics): + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context) -> ServerAppComponents: + net = IncomeClassifier() + params = ndarrays_to_parameters(get_weights(net)) + + strategy = FedAvg( + initial_parameters=params, + evaluate_metrics_aggregation_fn=weighted_average, + ) + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(config=config, strategy=strategy) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/fl-tabular/task.py b/examples/fl-tabular/fltabular/task.py similarity index 87% rename from examples/fl-tabular/task.py rename to examples/fl-tabular/fltabular/task.py index 3dc32cfa7105..f6e9c1a75adc 100644 --- a/examples/fl-tabular/task.py +++ b/examples/fl-tabular/fltabular/task.py @@ -1,3 +1,5 @@ +"""fltabular: Flower Example on Adult Census Income Tabular Dataset.""" + from collections import OrderedDict import torch @@ -9,9 +11,21 @@ from sklearn.pipeline import Pipeline from sklearn.preprocessing import OrdinalEncoder, StandardScaler from torch.utils.data import DataLoader, TensorDataset +from flwr_datasets.partitioner import IidPartitioner + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="scikit-learn/adult-census-income", + partitioners={"train": partitioner}, + ) -def load_data(partition_id: int, fds: FederatedDataset): dataset = fds.load_partition(partition_id, "train").with_format("pandas")[:] dataset.dropna(inplace=True) @@ -51,7 +65,7 @@ def load_data(partition_id: int, fds: FederatedDataset): class IncomeClassifier(nn.Module): - def __init__(self, input_dim: int): + def __init__(self, input_dim: int = 14): super(IncomeClassifier, self).__init__() self.layer1 = nn.Linear(input_dim, 128) self.layer2 = nn.Linear(128, 64) diff --git a/examples/fl-tabular/pyproject.toml b/examples/fl-tabular/pyproject.toml index 21498f73a4f3..04e8de41f0c7 100644 --- a/examples/fl-tabular/pyproject.toml +++ b/examples/fl-tabular/pyproject.toml @@ -4,17 +4,31 @@ build-backend = "hatchling.build" [project] name = "fl-tabular" -version = "0.1.0" +version = "1.0.0" description = "Adult Census Income Tabular Dataset and Federated Learning in Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets>=0.1.0,<1.0.0", + "flwr[simulation]>=1.10.0", + "flwr-datasets>=0.3.0", "torch==2.1.1", "scikit-learn==1.5.0", ] [tool.hatch.build.targets.wheel] packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "fltabular.server_app:app" +clientapp = "fltabular.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 5 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 5 \ No newline at end of file diff --git a/examples/fl-tabular/server.py b/examples/fl-tabular/server.py deleted file mode 100644 index 6b33d74d5b69..000000000000 --- a/examples/fl-tabular/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from flwr.common import ndarrays_to_parameters -from flwr.server import ServerApp, ServerConfig -from flwr.server.strategy import FedAvg - -from task import IncomeClassifier, get_weights - -net = IncomeClassifier(input_dim=14) -params = ndarrays_to_parameters(get_weights(net)) - - -def weighted_average(metrics): - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - return {"accuracy": sum(accuracies) / sum(examples)} - - -strategy = FedAvg( - initial_parameters=params, - evaluate_metrics_aggregation_fn=weighted_average, -) -app = ServerApp( - strategy=strategy, - config=ServerConfig(num_rounds=5), -) From e234d3b8a1cf9b449975540df41fdf1674509d86 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 23 Aug 2024 10:38:33 +0200 Subject: [PATCH 108/188] docs(framework:skip) Add Docker Compose quickstart docs (#3916) Signed-off-by: Robert Steiner --- doc/source/docker/index.rst | 8 + .../tutorial-quickstart-docker-compose.rst | 347 ++++++++++++++++++ .../docker/tutorial-quickstart-docker.rst | 1 + 3 files changed, 356 insertions(+) create mode 100644 doc/source/docker/tutorial-quickstart-docker-compose.rst diff --git a/doc/source/docker/index.rst b/doc/source/docker/index.rst index 389c42a23067..a070a47cb853 100644 --- a/doc/source/docker/index.rst +++ b/doc/source/docker/index.rst @@ -35,3 +35,11 @@ Advanced Options run-as-root-user pin-version use-a-different-version + +Run Flower Docker Compose +------------------------- + +.. toctree:: + :maxdepth: 1 + + tutorial-quickstart-docker-compose diff --git a/doc/source/docker/tutorial-quickstart-docker-compose.rst b/doc/source/docker/tutorial-quickstart-docker-compose.rst new file mode 100644 index 000000000000..e61fde358647 --- /dev/null +++ b/doc/source/docker/tutorial-quickstart-docker-compose.rst @@ -0,0 +1,347 @@ +Quickstart with Docker Compose +============================== + +This quickstart shows you how to set up Flower using Docker Compose in a single command, +allowing you to focus on developing your application without worrying about the underlying +infrastructure. + +You will also learn how to easily enable TLS encryption and persist application state locally, +giving you the freedom to choose the configuration that best suits your project's needs. + +Prerequisites +------------- + +Before you start, make sure that: + +- The ``flwr`` CLI is :doc:`installed <../how-to-install-flower>` locally. +- The Docker daemon is running. +- Docker Compose is `installed `_. + +Step 1: Set Up +-------------- + +#. Clone the Docker Compose ``complete`` directory: + + .. code-block:: bash + + $ git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/src/docker/complete . \ + && rm -rf _tmp && cd complete + +#. Create a new Flower project (PyTorch): + + .. code-block:: bash + + $ flwr new quickstart-compose --framework PyTorch --username flower + +#. Export the path of the newly created project. The path should be relative to the location of the + Docker Compose files: + + .. code-block:: bash + + $ export PROJECT_DIR=quickstart-compose + + Setting the ``PROJECT_DIR`` helps Docker Compose locate the ``pyproject.toml`` file, allowing + it to install dependencies in the SuperExec and SuperNode images correctly. + +Step 2: Run Flower in insecure mode +----------------------------------- + +To begin, start Flower with the most basic configuration. In this setup, Flower +will run without TLS and without persisting the state. + +.. note:: + + Without TLS, the data sent between the services remains **unencrypted**. Use it only for development + purposes. + + For production-oriented use cases, :ref:`enable TLS` for secure data transmission. + +Open your terminal and run: + +.. code-block:: bash + + $ docker compose -f compose.yml up --build -d + +.. dropdown:: Understand the command + + * ``docker compose``: The Docker command to run the Docker Compose tool. + * ``-f compose.yml``: Specify the YAML file that contains the basic Flower service definitions. + * ``--build``: Rebuild the images for each service if they don't already exist. + * ``-d``: Detach the containers from the terminal and run them in the background. + +Step 3: Run the Quickstart Project +---------------------------------- + +Now that the Flower services have been started via Docker Compose, it is time to run the +quickstart example. + +To ensure the ``flwr`` CLI connects to the SuperExec, you need to specify the SuperExec addresses +in the ``pyproject.toml`` file. + +#. Add the following lines to the ``quickstart-compose/pyproject.toml``: + + .. code-block:: toml + :caption: quickstart-compose/pyproject.toml + + [tool.flwr.federations.docker-compose] + address = "127.0.0.1:9093" + insecure = true + +#. Execute the command to run the quickstart example: + + .. code-block:: bash + + $ flwr run quickstart-compose docker-compose + +#. Monitor the SuperExec logs and wait for the summary to appear: + + .. code-block:: bash + + $ docker compose logs superexec -f + +Step 4: Update the Application +------------------------------ + +In the next step, change the application code. + +#. For example, go to the ``task.py`` file in the ``quickstart-compose/quickstart_compose/`` + directory and add a ``print`` call in the ``get_weights`` function: + + .. code-block:: python + :caption: quickstart-compose/quickstart_compose/task.py + + # ... + def get_weights(net): + print("Get weights") + return [val.cpu().numpy() for _, val in net.state_dict().items()] + # ... + +#. Rebuild and restart the services. + + .. note:: + + If you have modified the dependencies listed in your ``pyproject.toml`` file, it is essential + to rebuild images. + + If you haven't made any changes, you can skip this step. + + Run the following command to rebuild and restart the services: + + .. code-block:: bash + + $ docker compose -f compose.yml up --build -d + +#. Run the updated quickstart example: + + .. code-block:: bash + + $ flwr run quickstart-compose docker-compose + $ docker compose logs superexec -f + + In the SuperExec logs, you should find the ``Get weights`` line: + + .. code-block:: + :emphasize-lines: 9 + + superexec-1 | INFO : Starting Flower SuperExec + superexec-1 | WARNING : Option `--insecure` was set. Starting insecure HTTP server. + superexec-1 | INFO : Starting Flower SuperExec gRPC server on 0.0.0.0:9093 + superexec-1 | INFO : ExecServicer.StartRun + superexec-1 | 🎊 Successfully installed quickstart-compose to /app/.flwr/apps/flower/quickstart-compose/1.0.0. + superexec-1 | INFO : Created run -6767165609169293507 + superexec-1 | INFO : Started run -6767165609169293507 + superexec-1 | WARNING : Option `--insecure` was set. Starting insecure HTTP client connected to superlink:9091. + superexec-1 | Get weights + superexec-1 | INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + +Step 5: Persisting the SuperLink State +-------------------------------------- + +In this step, Flower services are configured to persist the state of the SuperLink service, +ensuring that it maintains its state even after a restart. + +.. note:: + + When working with Docker Compose on Linux, you may need to create the ``state`` directory first + and change its ownership to ensure proper access and permissions. + + For more information, consult the following page: :doc:`persist-superlink-state`. + +#. Run the command: + + .. code-block:: bash + + $ docker compose -f compose.yml -f with-state.yml up --build -d + + .. dropdown:: Understand the command + + * ``docker compose``: The Docker command to run the Docker Compose tool. + * ``-f compose.yml``: Specify the YAML file that contains the basic Flower service definitions. + * | ``-f with-state.yml``: Specifies the path to an additional Docker Compose file that + | contains the configuration for persisting the SuperLink state. + | + | Docker merges Compose files according to `merging rules `_. + * ``--build``: Rebuild the images for each service if they don't already exist. + * ``-d``: Detach the containers from the terminal and run them in the background. + +#. Rerun the ``quickstart-compose`` project: + + .. code-block:: bash + + $ flwr run quickstart-compose docker-compose + +#. Check the content of the ``state`` directory: + + .. code-block:: bash + + $ ls state/ + state.db + + You should see a ``state.db`` file in the ``state`` directory. If you restart the service, the + state file will be used to restore the state from the previously saved data. This ensures that + the data persists even if the containers are stopped and started again. + +.. _TLS: + +Step 6: Run Flower with TLS +--------------------------- + +#. To demonstrate how to enable TLS, generate self-signed certificates using the ``certs.yml`` + Compose file. + + .. important:: + + These certificates should be used only for development purposes. + + For production environments, use a service like `Let's Encrypt `_ + to obtain your certificates. + + Run the command: + + .. code-block:: bash + + $ docker compose -f certs.yml up --build + +#. Add the following lines to the ``quickstart-compose/pyproject.toml``: + + .. code-block:: toml + :caption: quickstart-compose/pyproject.toml + + [tool.flwr.federations.docker-compose-tls] + address = "127.0.0.1:9093" + root-certificates = "superexec-certificates/ca.crt" + +#. Restart the services with TLS enabled: + + .. code-block:: bash + + $ docker compose -f compose.yml -f with-tls.yml up --build -d + +#. Rerun the ``quickstart-compose`` project: + + .. code-block:: bash + + $ flwr run quickstart-compose docker-compose-tls + $ docker compose logs superexec -f + +Step 7: Add another SuperNode +----------------------------- + +You can add more SuperNodes by duplicating the SuperNode definition in the ``compose.yml`` file. + +Just make sure to give each new SuperNode service a unique service name like ``supernode-3``, ``supernode-4``, etc. + +In ``compose.yml``, add the following: + +.. code-block:: yaml + :caption: compose.yml + + services: + # other service definitions + + supernode-3: + <<: *supernode + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] + +If you also want to enable TLS for the new SuperNodes, duplicate the SuperNode definition for +each new SuperNode service in the ``with-tls.yml`` file. + +Make sure that the names of the services match with the one in the ``compose.yml`` file. + +In ``with-tls.yml``, add the following: + +.. code-block:: yaml + :caption: with-tls.yml + + services: + # other service definitions + + supernode-3: *supernode + +Step 8: Persisting the SuperLink State and Enabling TLS +------------------------------------------------------- + +To run Flower with persisted SuperLink state and enabled TLS, a slight change in the ``with-state.yml`` +file is required: + +#. Comment out line 3 and uncomment line 4: + + .. code-block:: yaml + :caption: with-state.yml + :linenos: + :emphasize-lines: 3-4 + + services: + superlink: + # <<: *x-without-tls + <<: *x-with-tls + volumes: + - ./state/:/app/state/:rw + +#. Restart the services: + + .. code-block:: bash + + $ docker compose -f compose.yml -f with-tls.yml -f with-state.yml up --build -d + +#. Rerun the ``quickstart-compose`` project: + + .. code-block:: bash + + $ flwr run quickstart-compose docker-compose-tls + $ docker compose logs superexec -f + +Step 9: Merge Multiple Compose Files +------------------------------------ + +You can merge multiple Compose files into a single file. For instance, if you wish to combine +the basic configuration with the TLS configuration, execute the following command: + +.. code-block:: bash + + $ docker compose -f compose.yml \ + -f with-tls.yml config --no-path-resolution > my_compose.yml + +This will merge the contents of ``compose.yml`` and ``with-tls.yml`` into a new file called +``my_compose.yml``. + +Step 10: Clean Up +----------------- + +Remove all services and volumes: + +.. code-block:: bash + + $ docker compose down -v + $ docker compose -f certs.yml down -v diff --git a/doc/source/docker/tutorial-quickstart-docker.rst b/doc/source/docker/tutorial-quickstart-docker.rst index 13f15bc4fb72..29ae6d5f6a43 100644 --- a/doc/source/docker/tutorial-quickstart-docker.rst +++ b/doc/source/docker/tutorial-quickstart-docker.rst @@ -367,3 +367,4 @@ Where to Go Next * :doc:`enable-tls` * :doc:`persist-superlink-state` +* :doc:`tutorial-quickstart-docker-compose` From e4fc89ba66619b35ac59911664ad82ee989912a8 Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Fri, 23 Aug 2024 10:55:57 +0200 Subject: [PATCH 109/188] docs(framework:skip) Fix warnings and broken rendering on some doc pages (#4063) --- .../server/workflow/secure_aggregation/secagg_workflow.py | 1 + .../workflow/secure_aggregation/secaggplus_workflow.py | 1 + src/py/flwr/simulation/run_simulation.py | 7 +++---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/server/workflow/secure_aggregation/secagg_workflow.py b/src/py/flwr/server/workflow/secure_aggregation/secagg_workflow.py index f56423e4a0d0..cd955430a4f0 100644 --- a/src/py/flwr/server/workflow/secure_aggregation/secagg_workflow.py +++ b/src/py/flwr/server/workflow/secure_aggregation/secagg_workflow.py @@ -35,6 +35,7 @@ class SecAggWorkflow(SecAggPlusWorkflow): contributions to compute the weighted average of model parameters. The protocol involves four main stages: + - 'setup': Send SecAgg configuration to clients and collect their public keys. - 'share keys': Broadcast public keys among clients and collect encrypted secret key shares. diff --git a/src/py/flwr/server/workflow/secure_aggregation/secaggplus_workflow.py b/src/py/flwr/server/workflow/secure_aggregation/secaggplus_workflow.py index 58f26c85d7eb..322e32ed5019 100644 --- a/src/py/flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +++ b/src/py/flwr/server/workflow/secure_aggregation/secaggplus_workflow.py @@ -99,6 +99,7 @@ class SecAggPlusWorkflow: contributions to compute the weighted average of model parameters. The protocol involves four main stages: + - 'setup': Send SecAgg+ configuration to clients and collect their public keys. - 'share keys': Broadcast public keys among clients and collect encrypted secret key shares. diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 362fb3144053..2fe323d20090 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -209,9 +209,8 @@ def run_simulation( messages sent by the `ServerApp`. num_supernodes : int - Number of nodes that run a ClientApp. They can be sampled by a - Driver in the ServerApp and receive a Message describing what the ClientApp - should perform. + Number of nodes that run a ClientApp. They can be sampled by a Driver in the + ServerApp and receive a Message describing what the ClientApp should perform. backend_name : str (default: ray) A simulation backend that runs `ClientApp`s. @@ -239,7 +238,7 @@ def run_simulation( if enable_tf_gpu_growth: warn_deprecated_feature_with_example( "Passing `enable_tf_gpu_growth=True` is deprecated.", - example_message="Instead, set the `TF_FORCE_GPU_ALLOW_GROWTH` environmnet " + example_message="Instead, set the `TF_FORCE_GPU_ALLOW_GROWTH` environment " "variable to true.", code_example='import os;os.environ["TF_FORCE_GPU_ALLOW_GROWTH"]="true"' "\n\tflwr.simulation.run_simulationt(...)", From c87f98f9b747a98543aea710a52b53f15aa8f9db Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 23 Aug 2024 10:10:00 +0100 Subject: [PATCH 110/188] refactor(framework) Deprecate accessing `Context` via `Client.context` (#3797) --- src/py/flwr/client/client.py | 23 ++++++++++++++++++- src/py/flwr/client/numpy_client.py | 23 ++++++++++++++++++- .../fleet/vce/backend/raybackend_test.py | 10 +++++--- .../superlink/fleet/vce/vce_api_test.py | 9 ++++++-- .../ray_transport/ray_client_proxy_test.py | 10 ++++---- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/py/flwr/client/client.py b/src/py/flwr/client/client.py index 23a3755f3efe..dab9b6a26588 100644 --- a/src/py/flwr/client/client.py +++ b/src/py/flwr/client/client.py @@ -33,12 +33,13 @@ Parameters, Status, ) +from flwr.common.logger import warn_deprecated_feature_with_example class Client(ABC): """Abstract base class for Flower clients.""" - context: Context + _context: Context def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes: """Return set of client's properties. @@ -141,6 +142,26 @@ def evaluate(self, ins: EvaluateIns) -> EvaluateRes: metrics={}, ) + @property + def context(self) -> Context: + """Getter for `Context` client attribute.""" + warn_deprecated_feature_with_example( + "Accessing the context via the client's attribute is deprecated.", + example_message="Instead, pass it to the client's " + "constructor in your `client_fn()` which already " + "receives a context object.", + code_example="def client_fn(context: Context) -> Client:\n\n" + "\t\t# Your existing client_fn\n\n" + "\t\t# Pass `context` to the constructor\n" + "\t\treturn FlowerClient(context).to_client()", + ) + return self._context + + @context.setter + def context(self, context: Context) -> None: + """Setter for `Context` client attribute.""" + self._context = context + def get_context(self) -> Context: """Get the run context from this client.""" return self.context diff --git a/src/py/flwr/client/numpy_client.py b/src/py/flwr/client/numpy_client.py index 0247958d88a9..b21a51b38e9b 100644 --- a/src/py/flwr/client/numpy_client.py +++ b/src/py/flwr/client/numpy_client.py @@ -27,6 +27,7 @@ ndarrays_to_parameters, parameters_to_ndarrays, ) +from flwr.common.logger import warn_deprecated_feature_with_example from flwr.common.typing import ( Code, EvaluateIns, @@ -70,7 +71,7 @@ class NumPyClient(ABC): """Abstract base class for Flower clients using NumPy.""" - context: Context + _context: Context def get_properties(self, config: Config) -> Dict[str, Scalar]: """Return a client's set of properties. @@ -174,6 +175,26 @@ def evaluate( _ = (self, parameters, config) return 0.0, 0, {} + @property + def context(self) -> Context: + """Getter for `Context` client attribute.""" + warn_deprecated_feature_with_example( + "Accessing the context via the client's attribute is deprecated.", + example_message="Instead, pass it to the client's " + "constructor in your `client_fn()` which already " + "receives a context object.", + code_example="def client_fn(context: Context) -> Client:\n\n" + "\t\t# Your existing client_fn\n\n" + "\t\t# Pass `context` to the constructor\n" + "\t\treturn FlowerClient(context).to_client()", + ) + return self._context + + @context.setter + def context(self, context: Context) -> None: + """Setter for `Context` client attribute.""" + self._context = context + def get_context(self) -> Context: """Get the run context from this client.""" return self.context diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py index 2f62bf725907..1ed7860827a4 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py @@ -32,6 +32,7 @@ Message, MessageTypeLegacy, Metadata, + RecordSet, Scalar, ) from flwr.common.constant import PARTITION_ID_KEY @@ -43,18 +44,22 @@ class DummyClient(NumPyClient): """A dummy NumPyClient for tests.""" + def __init__(self, state: RecordSet) -> None: + self.client_state = state + def get_properties(self, config: Config) -> Dict[str, Scalar]: """Return properties by doing a simple calculation.""" result = float(config["factor"]) * pi # store something in context - self.context.state.configs_records["result"] = ConfigsRecord({"result": result}) + self.client_state.configs_records["result"] = ConfigsRecord({"result": result}) + return {"result": result} def get_dummy_client(context: Context) -> Client: # pylint: disable=unused-argument """Return a DummyClient converted to Client type.""" - return DummyClient().to_client() + return DummyClient(state=context.state).to_client() def _load_app() -> ClientApp: @@ -149,7 +154,6 @@ def test_backend_creation_submit_and_termination( content.configs_records["getpropertiesres.properties"]["result"] == expected_output ) - # Verify context is correct obtained_result_in_context = updated_context.state.configs_records["result"][ "result" diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py index 28ed23cf6509..76e8ac9156d2 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api_test.py @@ -37,6 +37,7 @@ Message, MessageTypeLegacy, Metadata, + RecordSet, Scalar, ) from flwr.common.recordset_compat import getpropertiesins_to_recordset @@ -53,18 +54,22 @@ class DummyClient(NumPyClient): """A dummy NumPyClient for tests.""" + def __init__(self, state: RecordSet) -> None: + self.client_state = state + def get_properties(self, config: Config) -> Dict[str, Scalar]: """Return properties by doing a simple calculation.""" result = float(config["factor"]) * pi # store something in context - self.context.state.configs_records["result"] = ConfigsRecord({"result": result}) + self.client_state.configs_records["result"] = ConfigsRecord({"result": result}) + return {"result": result} def get_dummy_client(context: Context) -> Client: # pylint: disable=unused-argument """Return a DummyClient converted to Client type.""" - return DummyClient().to_client() + return DummyClient(state=context.state).to_client() dummy_client_app = ClientApp( diff --git a/src/py/flwr/simulation/ray_transport/ray_client_proxy_test.py b/src/py/flwr/simulation/ray_transport/ray_client_proxy_test.py index a0df3fc1eb8e..1c2aa455d9cd 100644 --- a/src/py/flwr/simulation/ray_transport/ray_client_proxy_test.py +++ b/src/py/flwr/simulation/ray_transport/ray_client_proxy_test.py @@ -32,6 +32,7 @@ Message, MessageTypeLegacy, Metadata, + RecordSet, Scalar, ) from flwr.common.constant import NUM_PARTITIONS_KEY, PARTITION_ID_KEY @@ -55,15 +56,15 @@ class DummyClient(NumPyClient): """A dummy NumPyClient for tests.""" - def __init__(self, node_id: int) -> None: + def __init__(self, node_id: int, state: RecordSet) -> None: self.node_id = node_id + self.client_state = state def get_properties(self, config: Config) -> Dict[str, Scalar]: """Return properties by doing a simple calculation.""" result = self.node_id * pi - # store something in context - self.context.state.configs_records["result"] = ConfigsRecord( + self.client_state.configs_records["result"] = ConfigsRecord( {"result": str(result)} ) return {"result": result} @@ -71,7 +72,7 @@ def get_properties(self, config: Config) -> Dict[str, Scalar]: def get_dummy_client(context: Context) -> Client: """Return a DummyClient converted to Client type.""" - return DummyClient(context.node_id).to_client() + return DummyClient(context.node_id, state=context.state).to_client() def prep( @@ -185,7 +186,6 @@ def test_cid_consistency_all_submit_first_run_consistency() -> None: "result" ]["result"] ) - ray.shutdown() From dc85aabe02bad774605c2b75a8000f8c18dfaa41 Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 23 Aug 2024 10:44:45 +0100 Subject: [PATCH 111/188] refactor(framework) Pass `ClientApp` loading function to backend's build (#3988) --- .../superlink/fleet/vce/backend/backend.py | 3 +-- .../superlink/fleet/vce/backend/raybackend.py | 17 +++++++++++++---- .../fleet/vce/backend/raybackend_test.py | 14 +++++++------- .../flwr/server/superlink/fleet/vce/vce_api.py | 8 ++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/backend.py b/src/py/flwr/server/superlink/fleet/vce/backend/backend.py index 56f31f22eb47..89341c0d238f 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/backend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/backend.py @@ -33,7 +33,7 @@ def __init__(self, backend_config: BackendConfig) -> None: """Construct a backend.""" @abstractmethod - def build(self) -> None: + def build(self, app_fn: Callable[[], ClientApp]) -> None: """Build backend. Different components need to be in place before workers in a backend are ready @@ -60,7 +60,6 @@ def terminate(self) -> None: @abstractmethod def process_message( self, - app: Callable[[], ClientApp], message: Message, context: Context, ) -> Tuple[Message, Context]: diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py index 0ef2ef737a8f..acfb248a6366 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py @@ -16,7 +16,7 @@ import sys from logging import DEBUG, ERROR -from typing import Callable, Dict, Tuple, Union +from typing import Callable, Dict, Optional, Tuple, Union import ray @@ -63,6 +63,8 @@ def __init__( actor_kwargs=actor_kwargs, ) + self.app_fn: Optional[Callable[[], ClientApp]] = None + def _validate_client_resources(self, config: BackendConfig) -> ClientResourcesDict: client_resources_config = config.get(self.client_resources_key) client_resources: ClientResourcesDict = {} @@ -126,14 +128,15 @@ def is_worker_idle(self) -> bool: """Report whether the pool has idle actors.""" return self.pool.is_actor_available() - def build(self) -> None: + def build(self, app_fn: Callable[[], ClientApp]) -> None: """Build pool of Ray actors that this backend will submit jobs to.""" self.pool.add_actors_to_pool(self.pool.actors_capacity) + # Set ClientApp callable that ray actors will use + self.app_fn = app_fn log(DEBUG, "Constructed ActorPool with: %i actors", self.pool.num_actors) def process_message( self, - app: Callable[[], ClientApp], message: Message, context: Context, ) -> Tuple[Message, Context]: @@ -143,11 +146,17 @@ def process_message( """ partition_id = context.node_config[PARTITION_ID_KEY] + if self.app_fn is None: + raise ValueError( + "Unspecified function to load a `ClientApp`. " + "Call the backend's `build()` method before processing messages." + ) + try: # Submit a task to the pool future = self.pool.submit( lambda a, a_fn, mssg, cid, state: a.run.remote(a_fn, mssg, cid, state), - (app, message, str(partition_id), context), + (self.app_fn, message, str(partition_id), context), ) # Fetch result diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py index 1ed7860827a4..cdb11401c29c 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend_test.py @@ -68,10 +68,11 @@ def _load_app() -> ClientApp: def backend_build_process_and_termination( backend: RayBackend, - process_args: Optional[Tuple[Callable[[], ClientApp], Message, Context]] = None, + app_fn: Callable[[], ClientApp], + process_args: Optional[Tuple[Message, Context]] = None, ) -> Union[Tuple[Message, Context], None]: """Build, process job and terminate RayBackend.""" - backend.build() + backend.build(app_fn) to_return = None if process_args: @@ -125,7 +126,9 @@ def doCleanups(self) -> None: def test_backend_creation_and_termination(self) -> None: """Test creation of RayBackend and its termination.""" backend = RayBackend(backend_config={}) - backend_build_process_and_termination(backend=backend, process_args=None) + backend_build_process_and_termination( + backend=backend, app_fn=_load_app, process_args=None + ) def test_backend_creation_submit_and_termination( self, @@ -134,13 +137,10 @@ def test_backend_creation_submit_and_termination( """Test submitting a message to a given ClientApp.""" backend = RayBackend(backend_config={}) - # Define ClientApp - client_app_callable = client_app_loader - message, context, expected_output = _create_message_and_context() res = backend_build_process_and_termination( - backend=backend, process_args=(client_app_callable, message, context) + backend=backend, app_fn=client_app_loader, process_args=(message, context) ) if res is None: diff --git a/src/py/flwr/server/superlink/fleet/vce/vce_api.py b/src/py/flwr/server/superlink/fleet/vce/vce_api.py index 81eb6cb6569c..165c2de73c21 100644 --- a/src/py/flwr/server/superlink/fleet/vce/vce_api.py +++ b/src/py/flwr/server/superlink/fleet/vce/vce_api.py @@ -87,7 +87,6 @@ def _register_node_states( # pylint: disable=too-many-arguments,too-many-locals def worker( - app_fn: Callable[[], ClientApp], taskins_queue: "Queue[TaskIns]", taskres_queue: "Queue[TaskRes]", node_states: Dict[int, NodeState], @@ -110,9 +109,7 @@ def worker( message = message_from_taskins(task_ins) # Let backend process message - out_mssg, updated_context = backend.process_message( - app_fn, message, context - ) + out_mssg, updated_context = backend.process_message(message, context) # Update Context node_states[node_id].update_context( @@ -193,7 +190,7 @@ def run_api( backend = backend_fn() # Build backend - backend.build() + backend.build(app_fn) # Add workers (they submit Messages to Backend) state = state_factory.state() @@ -223,7 +220,6 @@ def run_api( _ = [ executor.submit( worker, - app_fn, taskins_queue, taskres_queue, node_states, From 799ffe9dc0ab7a11367441d4e88cbfeda2d010e6 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 23 Aug 2024 10:55:00 +0100 Subject: [PATCH 112/188] docs(framework) Add flower-supernode and flower-superexec to CLI ref (#4061) --- doc/source/ref-api-cli.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/doc/source/ref-api-cli.rst b/doc/source/ref-api-cli.rst index 4397ae056941..ff1a9606f58d 100644 --- a/doc/source/ref-api-cli.rst +++ b/doc/source/ref-api-cli.rst @@ -30,15 +30,15 @@ flower-superlink :func: _parse_args_run_superlink :prog: flower-superlink -.. _flower-driver-api-apiref: +.. _flower-supernode-apiref: -flower-client-app +flower-supernode ~~~~~~~~~~~~~~~~~ .. argparse:: :module: flwr.client.supernode.app - :func: _parse_args_run_client_app - :prog: flower-client-app + :func: _parse_args_run_supernode + :prog: flower-supernode .. _flower-server-app-apiref: @@ -49,3 +49,13 @@ flower-server-app :module: flwr.server.run_serverapp :func: _parse_args_run_server_app :prog: flower-server-app + +.. _flower-superexec-apiref: + +flower-superexec +~~~~~~~~~~~~~~~~~ + +.. argparse:: + :module: flwr.superexec.app + :func: _parse_args_run_superexec + :prog: flower-superexec \ No newline at end of file From 7b8a30d5178112ce4363279e94b77749a5d1debe Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 23 Aug 2024 12:03:31 +0200 Subject: [PATCH 113/188] feat(framework:skip) Add SuperNode Alpine Docker image (#4050) Signed-off-by: Robert Steiner --- dev/build-docker-image-matrix.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dev/build-docker-image-matrix.py b/dev/build-docker-image-matrix.py index bcee5ef1c90e..f9822574d0d5 100644 --- a/dev/build-docker-image-matrix.py +++ b/dev/build-docker-image-matrix.py @@ -158,8 +158,12 @@ def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: + generate_binary_images( "supernode", base_images, - tag_latest_ubuntu_with_flwr_version, - lambda image: image.distro.name == DistroName.UBUNTU, + tag_latest_alpine_with_flwr_version, + lambda image: image.distro.name == DistroName.UBUNTU + or ( + image.distro.name == DistroName.ALPINE + and image.python_version == LATEST_SUPPORTED_PYTHON_VERSION + ), ) # ubuntu images for each supported python version + generate_binary_images( From 5a2065350c7540ab843eab33dd92f9cfc4462a2c Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 23 Aug 2024 11:13:44 +0100 Subject: [PATCH 114/188] refactor(framework) Update `README.md` template (#4064) --- .../flwr/cli/new/templates/app/README.md.tpl | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/src/py/flwr/cli/new/templates/app/README.md.tpl b/src/py/flwr/cli/new/templates/app/README.md.tpl index 40d83f5161e3..32e95fc0763d 100644 --- a/src/py/flwr/cli/new/templates/app/README.md.tpl +++ b/src/py/flwr/cli/new/templates/app/README.md.tpl @@ -1,43 +1,20 @@ # $project_name: A Flower / $framework_str app -## Install dependencies +## Install dependencies and project ```bash -pip install . +pip install -e . ``` -## Run (Simulation Engine) +## Run with the Simulation Engine In the `$project_name` directory, use `flwr run` to run a local simulation: ```bash -flwr run +flwr run . ``` -## Run (Deployment Engine) +## Run with the Deployment Engine -### Start the SuperLink - -```bash -flower-superlink --insecure -``` - -### Start the long-running Flower client - -In a new terminal window, start the first long-running Flower client: - -```bash -flower-client-app client:app --insecure -``` - -In yet another new terminal window, start the second long-running Flower client: - -```bash -flower-client-app client:app --insecure -``` - -### Start the ServerApp - -```bash -flower-server-app server:app --insecure -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. From c5e16e181dad1710a1e73ee6d8662f948014ff02 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Fri, 23 Aug 2024 12:21:53 +0200 Subject: [PATCH 115/188] feat(framework) Check and exit if port is in use (#4060) --- src/py/flwr/common/address.py | 43 +++++++++++++++++++ .../superlink/fleet/grpc_bidi/grpc_server.py | 5 +++ 2 files changed, 48 insertions(+) diff --git a/src/py/flwr/common/address.py b/src/py/flwr/common/address.py index 1c6481b80a74..7a70925c0fc9 100644 --- a/src/py/flwr/common/address.py +++ b/src/py/flwr/common/address.py @@ -14,6 +14,7 @@ # ============================================================================== """Flower IP address utils.""" +import socket from ipaddress import ip_address from typing import Optional, Tuple @@ -57,3 +58,45 @@ def parse_address(address: str) -> Optional[Tuple[str, int, Optional[bool]]]: except ValueError: return None + + +def is_port_in_use(address: str) -> bool: + """Check if the port specified in address is in use. + + Parameters + ---------- + address : str + The string representation of a domain, an IPv4, or an IPV6 address + with the port number. + + For example, '127.0.0.1:8080', or `[::1]:8080`. + + Returns + ------- + bool + If the port provided is in use or can't be parsed, + the function will return True, otherwise it will return False. + """ + parsed_address = parse_address(address) + if not parsed_address: + return True + host, port, is_v6 = parsed_address + + if is_v6: + protocol = socket.AF_INET6 + else: + protocol = socket.AF_INET + + with socket.socket(protocol, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + if is_v6: + # For IPv6, provide `flowinfo` and `scopeid` as 0 + s.bind((host, port, 0, 0)) + else: + # For IPv4 + s.bind((host, port)) + except OSError: + return True + + return False diff --git a/src/py/flwr/server/superlink/fleet/grpc_bidi/grpc_server.py b/src/py/flwr/server/superlink/fleet/grpc_bidi/grpc_server.py index 1b4286c87b92..dd78acb72fb1 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +++ b/src/py/flwr/server/superlink/fleet/grpc_bidi/grpc_server.py @@ -23,6 +23,7 @@ import grpc from flwr.common import GRPC_MAX_MESSAGE_LENGTH +from flwr.common.address import is_port_in_use from flwr.common.logger import log from flwr.proto.transport_pb2_grpc import ( # pylint: disable=E0611 add_FlowerServiceServicer_to_server, @@ -218,6 +219,10 @@ def generic_create_grpc_server( # pylint: disable=too-many-arguments server : grpc.Server A non-running instance of a gRPC server. """ + # Check if port is in use + if is_port_in_use(server_address): + sys.exit(f"Port in server address {server_address} is already in use.") + # Deconstruct tuple into servicer and function servicer, add_servicer_to_server_fn = servicer_and_add_fn From d2ffc147c4c8c733ec529c5fe7e174ce790956be Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 23 Aug 2024 12:23:10 +0100 Subject: [PATCH 116/188] feat(framework) Support Fleet API `GetFab` in `grpc-adapter` transport (#4067) --- src/py/flwr/server/app.py | 3 +++ .../fleet/grpc_adapter/grpc_adapter_servicer.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/app.py b/src/py/flwr/server/app.py index 3a7a135ee6fb..59e626653b52 100644 --- a/src/py/flwr/server/app.py +++ b/src/py/flwr/server/app.py @@ -310,6 +310,7 @@ def run_superlink() -> None: fleet_server = _run_fleet_api_grpc_adapter( address=fleet_address, state_factory=state_factory, + ffs_factory=ffs_factory, certificates=certificates, ) grpc_servers.append(fleet_server) @@ -516,12 +517,14 @@ def _run_fleet_api_grpc_rere( def _run_fleet_api_grpc_adapter( address: str, state_factory: StateFactory, + ffs_factory: FfsFactory, certificates: Optional[Tuple[bytes, bytes, bytes]], ) -> grpc.Server: """Run Fleet API (GrpcAdapter).""" # Create Fleet API gRPC server fleet_servicer = GrpcAdapterServicer( state_factory=state_factory, + ffs_factory=ffs_factory, ) fleet_add_servicer_to_server_fn = add_GrpcAdapterServicer_to_server fleet_grpc_server = generic_create_grpc_server( diff --git a/src/py/flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py b/src/py/flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py index 9325041061ac..278e20eb1d69 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +++ b/src/py/flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py @@ -23,6 +23,7 @@ from flwr.common.logger import log from flwr.proto import grpcadapter_pb2_grpc # pylint: disable=E0611 +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, CreateNodeResponse, @@ -37,6 +38,7 @@ ) from flwr.proto.grpcadapter_pb2 import MessageContainer # pylint: disable=E0611 from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 +from flwr.server.superlink.ffs.ffs_factory import FfsFactory from flwr.server.superlink.fleet.message_handler import message_handler from flwr.server.superlink.state import StateFactory @@ -60,10 +62,11 @@ def _handle( class GrpcAdapterServicer(grpcadapter_pb2_grpc.GrpcAdapterServicer): """Fleet API via GrpcAdapter servicer.""" - def __init__(self, state_factory: StateFactory) -> None: + def __init__(self, state_factory: StateFactory, ffs_factory: FfsFactory) -> None: self.state_factory = state_factory + self.ffs_factory = ffs_factory - def SendReceive( + def SendReceive( # pylint: disable=too-many-return-statements self, request: MessageContainer, context: grpc.ServicerContext ) -> MessageContainer: """.""" @@ -80,6 +83,8 @@ def SendReceive( return _handle(request, PushTaskResRequest, self._push_task_res) if request.grpc_message_name == GetRunRequest.__qualname__: return _handle(request, GetRunRequest, self._get_run) + if request.grpc_message_name == GetFabRequest.__qualname__: + return _handle(request, GetFabRequest, self._get_fab) raise ValueError(f"Invalid grpc_message_name: {request.grpc_message_name}") def _create_node(self, request: CreateNodeRequest) -> CreateNodeResponse: @@ -129,3 +134,11 @@ def _get_run(self, request: GetRunRequest) -> GetRunResponse: request=request, state=self.state_factory.state(), ) + + def _get_fab(self, request: GetFabRequest) -> GetFabResponse: + """Get FAB.""" + log(INFO, "GrpcAdapter.GetFab") + return message_handler.get_fab( + request=request, + ffs=self.ffs_factory.ffs(), + ) From cca0fab34e32ae6ae84de0a0b6cc275a7078dd40 Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 23 Aug 2024 12:41:46 +0100 Subject: [PATCH 117/188] feat(framework) Enable configuring the simulation backend via `flwr run` (#4059) --- src/py/flwr/cli/run/run.py | 10 ++++++++++ src/py/flwr/simulation/run_simulation.py | 24 ++++++++++++++++++++---- src/py/flwr/superexec/simulation.py | 21 ++++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index ff7aeac226b2..b2c4dc4151cd 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -15,6 +15,7 @@ """Flower command line interface `run` command.""" import hashlib +import json import subprocess import sys from logging import DEBUG @@ -192,6 +193,8 @@ def _run_without_superexec( ) -> None: try: num_supernodes = federation_config["options"]["num-supernodes"] + verbose: Optional[bool] = federation_config["options"].get("verbose") + backend_cfg = federation_config["options"].get("backend", {}) except KeyError as err: typer.secho( "❌ The project's `pyproject.toml` needs to declare the number of" @@ -212,6 +215,13 @@ def _run_without_superexec( f"{num_supernodes}", ] + if backend_cfg: + # Stringify as JSON + command.extend(["--backend-config", json.dumps(backend_cfg)]) + + if verbose: + command.extend(["--verbose"]) + if config_overrides: command.extend(["--run-config", f"{' '.join(config_overrides)}"]) diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 2fe323d20090..38edc7c24907 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -25,7 +25,7 @@ from logging import DEBUG, ERROR, INFO, WARNING from pathlib import Path from time import sleep -from typing import List, Optional +from typing import Any, List, Optional from flwr.cli.config_utils import load_and_validate from flwr.client import ClientApp @@ -91,6 +91,17 @@ def _resolve_message(conflict_keys: List[str]) -> str: return True +def _replace_keys(d: Any, match: str, target: str) -> Any: + if isinstance(d, dict): + return { + k.replace(match, target): _replace_keys(v, match, target) + for k, v in d.items() + } + if isinstance(d, list): + return [_replace_keys(i, match, target) for i in d] + return d + + # Entry point from CLI # pylint: disable=too-many-locals def run_simulation_from_cli() -> None: @@ -105,6 +116,14 @@ def run_simulation_from_cli() -> None: code_example='TF_FORCE_GPU_ALLOW_GROWTH="true" flower-simulation <...>', ) + # Load JSON config + backend_config_dict = json.loads(args.backend_config) + + if backend_config_dict: + # Backend config internally operates with `_` not with `-` + backend_config_dict = _replace_keys(backend_config_dict, match="-", target="_") + log(DEBUG, "backend_config_dict: %s", backend_config_dict) + # We are supporting two modes for the CLI entrypoint: # 1) Running an app dir containing a `pyproject.toml` # 2) Running any ClientApp and SeverApp w/o pyproject.toml being present @@ -167,9 +186,6 @@ def run_simulation_from_cli() -> None: override_config=override_config, ) - # Load JSON config - backend_config_dict = json.loads(args.backend_config) - _run_simulation( server_app_attr=server_app_attr, client_app_attr=client_app_attr, diff --git a/src/py/flwr/superexec/simulation.py b/src/py/flwr/superexec/simulation.py index a2048f179938..e913b6812556 100644 --- a/src/py/flwr/superexec/simulation.py +++ b/src/py/flwr/superexec/simulation.py @@ -15,6 +15,7 @@ """Simulation engine executor.""" +import json import subprocess import sys from logging import ERROR, INFO, WARN @@ -24,6 +25,7 @@ from flwr.cli.config_utils import load_and_validate from flwr.cli.install import install_from_fab +from flwr.common.config import unflatten_dict from flwr.common.constant import RUN_ID_NUM_BYTES from flwr.common.logger import log from flwr.common.typing import UserConfig @@ -108,6 +110,7 @@ def set_config( ) self.verbose = verbose + # pylint: disable=too-many-locals @override def start_run( self, @@ -152,6 +155,15 @@ def start_run( "Config extracted from FAB's pyproject.toml is not valid" ) + # Flatten federated config + federation_config_flat = unflatten_dict(federation_config) + + num_supernodes = federation_config_flat.get( + "num-supernodes", self.num_supernodes + ) + backend_cfg = federation_config_flat.get("backend", {}) + verbose: Optional[bool] = federation_config_flat.get("verbose") + # In Simulation there is no SuperLink, still we create a run_id run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) log(INFO, "Created run %s", str(run_id)) @@ -162,11 +174,18 @@ def start_run( "--app", f"{str(fab_path)}", "--num-supernodes", - f"{federation_config.get('num-supernodes', self.num_supernodes)}", + f"{num_supernodes}", "--run-id", str(run_id), ] + if backend_cfg: + # Stringify as JSON + command.extend(["--backend-config", json.dumps(backend_cfg)]) + + if verbose: + command.extend(["--verbose"]) + if override_config: override_config_str = _user_config_to_str(override_config) command.extend(["--run-config", f"{override_config_str}"]) From b9d8e00b02af7c0d25e4fbd5424b859d72ec4752 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 23 Aug 2024 14:17:04 +0200 Subject: [PATCH 118/188] feat(framework) Add all-in-one Docker Compose (#3784) Signed-off-by: Robert Steiner --- src/docker/complete/.gitignore | 3 + src/docker/complete/certs.yml | 124 +++++++++++++++++++++++++++++ src/docker/complete/compose.yml | 117 +++++++++++++++++++++++++++ src/docker/complete/with-state.yml | 14 ++++ src/docker/complete/with-tls.yml | 78 ++++++++++++++++++ 5 files changed, 336 insertions(+) create mode 100644 src/docker/complete/.gitignore create mode 100644 src/docker/complete/certs.yml create mode 100644 src/docker/complete/compose.yml create mode 100644 src/docker/complete/with-state.yml create mode 100644 src/docker/complete/with-tls.yml diff --git a/src/docker/complete/.gitignore b/src/docker/complete/.gitignore new file mode 100644 index 000000000000..53a6b57b9b4d --- /dev/null +++ b/src/docker/complete/.gitignore @@ -0,0 +1,3 @@ +superexec-certificates +superlink-certificates +state diff --git a/src/docker/complete/certs.yml b/src/docker/complete/certs.yml new file mode 100644 index 000000000000..d7d938a2aa4a --- /dev/null +++ b/src/docker/complete/certs.yml @@ -0,0 +1,124 @@ +services: + gen-certs: + build: + context: . + pull: true + dockerfile_inline: | + FROM ubuntu:latest + + RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + openssl + + WORKDIR /app/script + + ARG SUPERLINK_IP=127.0.0.1 + + COPY <<-EOF superlink-certificate.conf + [req] + default_bits = 4096 + prompt = no + default_md = sha256 + req_extensions = req_ext + distinguished_name = dn + + [dn] + C = US + O = Flower + CN = localhost + + [req_ext] + subjectAltName = @alt_names + + [alt_names] + DNS.0 = superlink + IP.1 = ::1 + IP.2 = $${SUPERLINK_IP} + EOF + + ARG SUPEREXEC_IP=127.0.0.1 + + COPY <<-EOF superexec-certificate.conf + [req] + default_bits = 4096 + prompt = no + default_md = sha256 + req_extensions = req_ext + distinguished_name = dn + + [dn] + C = US + O = Flower + CN = localhost + + [req_ext] + subjectAltName = @alt_names + + [alt_names] + DNS.0 = superexec + IP.1 = ::1 + IP.2 = $${SUPEREXEC_IP} + EOF + + COPY --chmod=744 <<-'EOF' generate.sh + #!/bin/bash + # This script will generate all certificates if ca.crt does not exist + + set -e + cd "$$( cd "$$( dirname "$${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ + + CA_PASSWORD=notsafe + + # Generate directories if not exists + + generate () { + mkdir -p "$$1" + + if [ -f ""$$1"/ca.crt" ]; then + echo "Skipping certificate generation as they already exist." + return 0 + fi + + # Generate the root certificate authority key and certificate based on key + openssl genrsa -out "$$1"/ca.key 4096 + openssl req \ + -new \ + -x509 \ + -key "$$1"/ca.key \ + -sha256 \ + -subj "/C=DE/ST=HH/O=CA, Inc." \ + -days 365 -out "$$1"/ca.crt + + # Generate a new private key for the server + openssl genrsa -out "$$1"/server.key 4096 + + # Create a signing CSR + openssl req \ + -new \ + -key "$$1"/server.key \ + -out "$$1"/server.csr \ + -config ./script/"$$2" + + # Generate a certificate for the server + openssl x509 \ + -req \ + -in "$$1"/server.csr \ + -CA "$$1"/ca.crt \ + -CAkey "$$1"/ca.key \ + -CAcreateserial \ + -out "$$1"/server.pem \ + -days 365 \ + -sha256 \ + -extfile ./script/"$$2" \ + -extensions req_ext + } + generate superlink-certificates superlink-certificate.conf + generate superexec-certificates superexec-certificate.conf + EOF + + WORKDIR /app + + ENTRYPOINT ["./script/generate.sh"] + volumes: + - ./superlink-certificates/:/app/superlink-certificates/:rw + - ./superexec-certificates/:/app/superexec-certificates/:rw diff --git a/src/docker/complete/compose.yml b/src/docker/complete/compose.yml new file mode 100644 index 000000000000..90261249f322 --- /dev/null +++ b/src/docker/complete/compose.yml @@ -0,0 +1,117 @@ +services: + # create a SuperLink service + superlink: + image: flwr/superlink:${FLWR_VERSION:-1.10.0} + command: + - --insecure + + # create a SuperExec service + superexec: + user: root + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/superexec:${FLWR_VERSION:-1.10.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-superexec"] + ports: + - 9093:9093 + command: + - --executor + - flwr.superexec.deployment:executor + - --insecure + - --executor-config + - superlink="superlink:9091" + depends_on: + - superlink + volumes: + - apps-volume:/app/.flwr/apps/:rw + + # create a two SuperNode service with different node configs + supernode-1: + user: root + deploy: + resources: + limits: + cpus: "2" + command: + - --superlink + - superlink:9092 + - --insecure + depends_on: + - superlink + volumes: + - apps-volume:/app/.flwr/apps/:ro + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] + + supernode-2: + user: root + deploy: + resources: + limits: + cpus: "2" + command: + - --superlink + - superlink:9092 + - --insecure + depends_on: + - superlink + volumes: + - apps-volume:/app/.flwr/apps/:ro + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=1,num-partitions=2"] + + # uncomment to add another supernode + # + # supernode-3: + # user: root + # deploy: + # resources: + # limits: + # cpus: "2" + # command: + # - --superlink + # - superlink:9092 + # - --insecure + # depends_on: + # - superlink + # volumes: + # - apps-volume:/app/.flwr/apps/:ro + # build: + # context: ${PROJECT_DIR:-.} + # dockerfile_inline: | + # FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + + # WORKDIR /app + # COPY --chown=app:app pyproject.toml . + # RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + # && python -m pip install -U --no-cache-dir . + + # ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] + +volumes: + apps-volume: diff --git a/src/docker/complete/with-state.yml b/src/docker/complete/with-state.yml new file mode 100644 index 000000000000..cc922a9ef12e --- /dev/null +++ b/src/docker/complete/with-state.yml @@ -0,0 +1,14 @@ +services: + superlink: + command: + - --insecure + - --database=state/state.db + # To toggle TLS encryption and persisting state for the SuperLink, comment the key `command` + # above and uncomment the lines below: + # command: + # - --ssl-ca-certfile=certificates/ca.crt + # - --ssl-certfile=certificates/server.pem + # - --ssl-keyfile=certificates/server.key + # - --database=state/state.db + volumes: + - ./state/:/app/state/:rw diff --git a/src/docker/complete/with-tls.yml b/src/docker/complete/with-tls.yml new file mode 100644 index 000000000000..1b8540e09b64 --- /dev/null +++ b/src/docker/complete/with-tls.yml @@ -0,0 +1,78 @@ +services: + superlink: + command: + - --ssl-ca-certfile=certificates/ca.crt + - --ssl-certfile=certificates/server.pem + - --ssl-keyfile=certificates/server.key + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + - source: superlink-certfile + target: /app/certificates/server.pem + - source: superlink-keyfile + target: /app/certificates/server.key + + superexec: + command: + - --executor + - flwr.superexec.deployment:executor + - --executor-config + - superlink="superlink:9091",root-certificates="certificates/superlink-ca.crt" + - --ssl-ca-certfile=certificates/ca.crt + - --ssl-certfile=certificates/server.pem + - --ssl-keyfile=certificates/server.key + secrets: + - source: superlink-ca-certfile + target: /app/certificates/superlink-ca.crt + - source: superexec-ca-certfile + target: /app/certificates/ca.crt + - source: superexec-certfile + target: /app/certificates/server.pem + - source: superexec-keyfile + target: /app/certificates/server.key + + supernode-1: + command: + - --superlink + - superlink:9092 + - --root-certificates + - certificates/ca.crt + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + + supernode-2: + command: + - --superlink + - superlink:9092 + - --root-certificates + - certificates/ca.crt + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + + # uncomment to enable TLS on another supernode + # + # supernode-3: + # command: + # - --superlink + # - superlink:9092 + # - --root-certificates + # - certificates/ca.crt + # secrets: + # - source: superlink-ca-certfile + # target: /app/certificates/ca.crt + +secrets: + superlink-ca-certfile: + file: ./superlink-certificates/ca.crt + superlink-certfile: + file: ./superlink-certificates/server.pem + superlink-keyfile: + file: ./superlink-certificates/server.key + superexec-ca-certfile: + file: ./superexec-certificates/ca.crt + superexec-certfile: + file: ./superexec-certificates/server.pem + superexec-keyfile: + file: ./superexec-certificates/server.key From 59af7188cd1aaf45eceeb4a385fc841895c68ed6 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 23 Aug 2024 14:46:38 +0200 Subject: [PATCH 119/188] incorporate compose changes (#4066) Signed-off-by: Robert Steiner --- .../tutorial-quickstart-docker-compose.rst | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/doc/source/docker/tutorial-quickstart-docker-compose.rst b/doc/source/docker/tutorial-quickstart-docker-compose.rst index e61fde358647..93a000295951 100644 --- a/doc/source/docker/tutorial-quickstart-docker-compose.rst +++ b/doc/source/docker/tutorial-quickstart-docker-compose.rst @@ -261,7 +261,19 @@ In ``compose.yml``, add the following: # other service definitions supernode-3: - <<: *supernode + user: root + deploy: + resources: + limits: + cpus: "2" + command: + - --superlink + - superlink:9092 + - --insecure + depends_on: + - superlink + volumes: + - apps-volume:/app/.flwr/apps/:ro build: context: ${PROJECT_DIR:-.} dockerfile_inline: | @@ -287,7 +299,15 @@ In ``with-tls.yml``, add the following: services: # other service definitions - supernode-3: *supernode + supernode-3: + command: + - --superlink + - superlink:9092 + - --root-certificates + - certificates/ca.crt + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt Step 8: Persisting the SuperLink State and Enabling TLS ------------------------------------------------------- @@ -295,17 +315,23 @@ Step 8: Persisting the SuperLink State and Enabling TLS To run Flower with persisted SuperLink state and enabled TLS, a slight change in the ``with-state.yml`` file is required: -#. Comment out line 3 and uncomment line 4: +#. Comment out the lines 3-5 and uncomment the lines 6-10: .. code-block:: yaml :caption: with-state.yml :linenos: - :emphasize-lines: 3-4 + :emphasize-lines: 3-10 services: superlink: - # <<: *x-without-tls - <<: *x-with-tls + # command: + # - --insecure + # - --database=state/state.db + command: + - --ssl-ca-certfile=certificates/ca.crt + - --ssl-certfile=certificates/server.pem + - --ssl-keyfile=certificates/server.key + - --database=state/state.db volumes: - ./state/:/app/state/:rw From d580e992d6739ea3020f438a68dd139f4368ecf3 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 23 Aug 2024 14:23:22 +0100 Subject: [PATCH 120/188] feat(framework) Support Fleet API `GetFab` in REST transport layer and reduce redundancy (#4068) --- src/py/flwr/server/app.py | 3 + .../superlink/fleet/rest_rere/rest_api.py | 193 +++++++----------- 2 files changed, 74 insertions(+), 122 deletions(-) diff --git a/src/py/flwr/server/app.py b/src/py/flwr/server/app.py index 59e626653b52..ef632a0c014d 100644 --- a/src/py/flwr/server/app.py +++ b/src/py/flwr/server/app.py @@ -271,6 +271,7 @@ def run_superlink() -> None: ssl_keyfile, ssl_certfile, state_factory, + ffs_factory, num_workers, ), ) @@ -547,6 +548,7 @@ def _run_fleet_api_rest( ssl_keyfile: Optional[str], ssl_certfile: Optional[str], state_factory: StateFactory, + ffs_factory: FfsFactory, num_workers: int, ) -> None: """Run Driver API (REST-based).""" @@ -561,6 +563,7 @@ def _run_fleet_api_rest( # See: https://www.starlette.io/applications/#accessing-the-app-instance fast_api_app.state.STATE_FACTORY = state_factory + fast_api_app.state.FFS_FACTORY = ffs_factory uvicorn.run( app="flwr.server.superlink.fleet.rest_rere.rest_api:app", diff --git a/src/py/flwr/server/superlink/fleet/rest_rere/rest_api.py b/src/py/flwr/server/superlink/fleet/rest_rere/rest_api.py index 1ed67e5eb0aa..cf5ad16f7999 100644 --- a/src/py/flwr/server/superlink/fleet/rest_rere/rest_api.py +++ b/src/py/flwr/server/superlink/fleet/rest_rere/rest_api.py @@ -15,17 +15,29 @@ """Experimental REST API server.""" +from __future__ import annotations + import sys +from typing import Awaitable, Callable, TypeVar + +from google.protobuf.message import Message as GrpcMessage from flwr.common.constant import MISSING_EXTRA_REST +from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611 from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611 CreateNodeRequest, + CreateNodeResponse, DeleteNodeRequest, + DeleteNodeResponse, PingRequest, + PingResponse, PullTaskInsRequest, + PullTaskInsResponse, PushTaskResRequest, + PushTaskResResponse, ) -from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611 +from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611 +from flwr.server.superlink.ffs.ffs import Ffs from flwr.server.superlink.fleet.message_handler import message_handler from flwr.server.superlink.state import State @@ -40,172 +52,108 @@ sys.exit(MISSING_EXTRA_REST) -async def create_node(request: Request) -> Response: - """Create Node.""" - _check_headers(request.headers) +GrpcRequest = TypeVar("GrpcRequest", bound=GrpcMessage) +GrpcResponse = TypeVar("GrpcResponse", bound=GrpcMessage) - # Get the request body as raw bytes - create_node_request_bytes: bytes = await request.body() +GrpcAsyncFunction = Callable[[GrpcRequest], Awaitable[GrpcResponse]] +RestEndPoint = Callable[[Request], Awaitable[Response]] - # Deserialize ProtoBuf - create_node_request_proto = CreateNodeRequest() - create_node_request_proto.ParseFromString(create_node_request_bytes) - # Get state from app - state: State = app.state.STATE_FACTORY.state() +def rest_request_response( + grpc_request_type: type[GrpcRequest], +) -> Callable[[GrpcAsyncFunction[GrpcRequest, GrpcResponse]], RestEndPoint]: + """Convert an async gRPC-based function into a RESTful HTTP endpoint.""" - # Handle message - create_node_response_proto = message_handler.create_node( - request=create_node_request_proto, state=state - ) + def decorator(func: GrpcAsyncFunction[GrpcRequest, GrpcResponse]) -> RestEndPoint: + async def wrapper(request: Request) -> Response: + _check_headers(request.headers) - # Return serialized ProtoBuf - create_node_response_bytes = create_node_response_proto.SerializeToString() - return Response( - status_code=200, - content=create_node_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) + # Get the request body as raw bytes + grpc_req_bytes: bytes = await request.body() + # Deserialize ProtoBuf + grpc_req = grpc_request_type.FromString(grpc_req_bytes) + grpc_res = await func(grpc_req) + return Response( + status_code=200, + content=grpc_res.SerializeToString(), + headers={"Content-Type": "application/protobuf"}, + ) -async def delete_node(request: Request) -> Response: - """Delete Node Id.""" - _check_headers(request.headers) + return wrapper - # Get the request body as raw bytes - delete_node_request_bytes: bytes = await request.body() + return decorator - # Deserialize ProtoBuf - delete_node_request_proto = DeleteNodeRequest() - delete_node_request_proto.ParseFromString(delete_node_request_bytes) +@rest_request_response(CreateNodeRequest) +async def create_node(request: CreateNodeRequest) -> CreateNodeResponse: + """Create Node.""" # Get state from app state: State = app.state.STATE_FACTORY.state() # Handle message - delete_node_response_proto = message_handler.delete_node( - request=delete_node_request_proto, state=state - ) + return message_handler.create_node(request=request, state=state) - # Return serialized ProtoBuf - delete_node_response_bytes = delete_node_response_proto.SerializeToString() - return Response( - status_code=200, - content=delete_node_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) +@rest_request_response(DeleteNodeRequest) +async def delete_node(request: DeleteNodeRequest) -> DeleteNodeResponse: + """Delete Node Id.""" + # Get state from app + state: State = app.state.STATE_FACTORY.state() -async def pull_task_ins(request: Request) -> Response: - """Pull TaskIns.""" - _check_headers(request.headers) - - # Get the request body as raw bytes - pull_task_ins_request_bytes: bytes = await request.body() + # Handle message + return message_handler.delete_node(request=request, state=state) - # Deserialize ProtoBuf - pull_task_ins_request_proto = PullTaskInsRequest() - pull_task_ins_request_proto.ParseFromString(pull_task_ins_request_bytes) +@rest_request_response(PullTaskInsRequest) +async def pull_task_ins(request: PullTaskInsRequest) -> PullTaskInsResponse: + """Pull TaskIns.""" # Get state from app state: State = app.state.STATE_FACTORY.state() # Handle message - pull_task_ins_response_proto = message_handler.pull_task_ins( - request=pull_task_ins_request_proto, - state=state, - ) - - # Return serialized ProtoBuf - pull_task_ins_response_bytes = pull_task_ins_response_proto.SerializeToString() - return Response( - status_code=200, - content=pull_task_ins_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) + return message_handler.pull_task_ins(request=request, state=state) -async def push_task_res(request: Request) -> Response: # Check if token is needed here +# Check if token is needed here +@rest_request_response(PushTaskResRequest) +async def push_task_res(request: PushTaskResRequest) -> PushTaskResResponse: """Push TaskRes.""" - _check_headers(request.headers) - - # Get the request body as raw bytes - push_task_res_request_bytes: bytes = await request.body() - - # Deserialize ProtoBuf - push_task_res_request_proto = PushTaskResRequest() - push_task_res_request_proto.ParseFromString(push_task_res_request_bytes) - # Get state from app state: State = app.state.STATE_FACTORY.state() # Handle message - push_task_res_response_proto = message_handler.push_task_res( - request=push_task_res_request_proto, - state=state, - ) - - # Return serialized ProtoBuf - push_task_res_response_bytes = push_task_res_response_proto.SerializeToString() - return Response( - status_code=200, - content=push_task_res_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) + return message_handler.push_task_res(request=request, state=state) -async def ping(request: Request) -> Response: +@rest_request_response(PingRequest) +async def ping(request: PingRequest) -> PingResponse: """Ping.""" - _check_headers(request.headers) - - # Get the request body as raw bytes - ping_request_bytes: bytes = await request.body() - - # Deserialize ProtoBuf - ping_request_proto = PingRequest() - ping_request_proto.ParseFromString(ping_request_bytes) - # Get state from app state: State = app.state.STATE_FACTORY.state() # Handle message - ping_response_proto = message_handler.ping(request=ping_request_proto, state=state) - - # Return serialized ProtoBuf - ping_response_bytes = ping_response_proto.SerializeToString() - return Response( - status_code=200, - content=ping_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) + return message_handler.ping(request=request, state=state) -async def get_run(request: Request) -> Response: +@rest_request_response(GetRunRequest) +async def get_run(request: GetRunRequest) -> GetRunResponse: """GetRun.""" - _check_headers(request.headers) - - # Get the request body as raw bytes - get_run_request_bytes: bytes = await request.body() - - # Deserialize ProtoBuf - get_run_request_proto = GetRunRequest() - get_run_request_proto.ParseFromString(get_run_request_bytes) - # Get state from app state: State = app.state.STATE_FACTORY.state() # Handle message - get_run_response_proto = message_handler.get_run( - request=get_run_request_proto, state=state - ) + return message_handler.get_run(request=request, state=state) + - # Return serialized ProtoBuf - get_run_response_bytes = get_run_response_proto.SerializeToString() - return Response( - status_code=200, - content=get_run_response_bytes, - headers={"Content-Type": "application/protobuf"}, - ) +@rest_request_response(GetFabRequest) +async def get_fab(request: GetFabRequest) -> GetFabResponse: + """GetRun.""" + # Get ffs from app + ffs: Ffs = app.state.FFS_FACTORY.state() + + # Handle message + return message_handler.get_fab(request=request, ffs=ffs) routes = [ @@ -215,6 +163,7 @@ async def get_run(request: Request) -> Response: Route("/api/v0/fleet/push-task-res", push_task_res, methods=["POST"]), Route("/api/v0/fleet/ping", ping, methods=["POST"]), Route("/api/v0/fleet/get-run", get_run, methods=["POST"]), + Route("/api/v0/fleet/get-fab", get_fab, methods=["POST"]), ] app: Starlette = Starlette( From 24efb180ac58199a0c7550449c70aab237b57ef8 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 23 Aug 2024 14:30:31 +0100 Subject: [PATCH 121/188] fix(framework:skip) Fix the API path for `delete_node` in REST transport (#4069) --- src/py/flwr/client/rest_client/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/client/rest_client/connection.py b/src/py/flwr/client/rest_client/connection.py index 8e5766bbc491..d5f005fbaf77 100644 --- a/src/py/flwr/client/rest_client/connection.py +++ b/src/py/flwr/client/rest_client/connection.py @@ -275,7 +275,7 @@ def delete_node() -> None: req = DeleteNodeRequest(node=node) # Send the request - res = _request(req, DeleteNodeResponse, PATH_CREATE_NODE) + res = _request(req, DeleteNodeResponse, PATH_DELETE_NODE) if res is None: return From 372b68b6496f0d1f6076167e3dbe44d4047579f3 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Fri, 23 Aug 2024 16:10:46 +0100 Subject: [PATCH 122/188] fix(framework) Edit `flower-simulation` args (#4071) --- src/py/flwr/simulation/run_simulation.py | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 38edc7c24907..af12da4a5814 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -35,6 +35,7 @@ from flwr.common.logger import ( set_logger_propagation, update_console_handler, + warn_deprecated_feature, warn_deprecated_feature_with_example, ) from flwr.common.typing import Run, UserConfig @@ -108,6 +109,19 @@ def run_simulation_from_cli() -> None: """Run Simulation Engine from the CLI.""" args = _parse_args_run_simulation().parse_args() + # Add warnings for deprecated server_app and client_app arguments + if args.server_app: + warn_deprecated_feature( + "The `--server-app` argument is deprecated. " + "Please use the `--app` argument instead." + ) + + if args.client_app: + warn_deprecated_feature( + "The `--client-app` argument is deprecated. " + "Use the `--app` argument instead." + ) + if args.enable_tf_gpu_growth: warn_deprecated_feature_with_example( "Passing `--enable-tf-gpu-growth` is deprecated.", @@ -527,13 +541,22 @@ def _parse_args_run_simulation() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Start a Flower simulation", ) + parser.add_argument( + "--app", + type=str, + default=None, + help="Path to a directory containing a FAB-like structure with a " + "pyproject.toml.", + ) parser.add_argument( "--server-app", - help="For example: `server:app` or `project.package.module:wrapper.app`", + help="(DEPRECATED: use --app instead) For example: `server:app` or " + "`project.package.module:wrapper.app`", ) parser.add_argument( "--client-app", - help="For example: `client:app` or `project.package.module:wrapper.app`", + help="(DEPRECATED: use --app instead) For example: `client:app` or " + "`project.package.module:wrapper.app`", ) parser.add_argument( "--num-supernodes", @@ -541,13 +564,6 @@ def _parse_args_run_simulation() -> argparse.ArgumentParser: required=True, help="Number of simulated SuperNodes.", ) - parser.add_argument( - "--app", - type=str, - default=None, - help="Path to a directory containing a FAB-like structure with a " - "pyproject.toml.", - ) parser.add_argument( "--run-config", default=None, From f44f3da3fac930f1ceb334dee4033796f88ba91b Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 23 Aug 2024 16:18:50 +0100 Subject: [PATCH 123/188] fix(framework) Read project name from `pyproject.toml` in `flwr build` (#4073) --- src/py/flwr/cli/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/cli/build.py b/src/py/flwr/cli/build.py index 62ff2fd77c12..676bc1723568 100644 --- a/src/py/flwr/cli/build.py +++ b/src/py/flwr/cli/build.py @@ -89,7 +89,7 @@ def build( # Set the name of the zip file fab_filename = ( f"{conf['tool']['flwr']['app']['publisher']}" - f".{app.name}" + f".{conf['project']['name']}" f".{conf['project']['version'].replace('.', '-')}.fab" ) list_file_content = "" From 49fe4775ef1176f2fd98994141af7b72e0023a59 Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:13:03 +0200 Subject: [PATCH 124/188] refactor(examples) Update federated kaplan meier fitter (#3922) Co-authored-by: jafermarq --- .../federated-kaplan-meier-fitter/README.md | 84 +++++++------------ .../examplefkm/__init__.py | 1 + .../{client.py => examplefkm/client_app.py} | 41 ++++----- .../{server.py => examplefkm/server_app.py} | 36 +++++--- .../examplefkm/task.py | 17 ++++ .../pyproject.toml | 43 +++++++--- .../requirements.txt | 5 -- 7 files changed, 118 insertions(+), 109 deletions(-) create mode 100644 examples/federated-kaplan-meier-fitter/examplefkm/__init__.py rename examples/federated-kaplan-meier-fitter/{client.py => examplefkm/client_app.py} (54%) rename examples/federated-kaplan-meier-fitter/{server.py => examplefkm/server_app.py} (83%) create mode 100644 examples/federated-kaplan-meier-fitter/examplefkm/task.py delete mode 100644 examples/federated-kaplan-meier-fitter/requirements.txt diff --git a/examples/federated-kaplan-meier-fitter/README.md b/examples/federated-kaplan-meier-fitter/README.md index 20d4ca4c47af..1964ec4e5653 100644 --- a/examples/federated-kaplan-meier-fitter/README.md +++ b/examples/federated-kaplan-meier-fitter/README.md @@ -4,9 +4,9 @@ dataset: [Waltons] framework: [lifelines] --- -# Flower Example using KaplanMeierFitter +# Federated Survival Analysis with Flower and KaplanMeierFitter -This is an introductory example on **federated survival analysis** using [Flower](https://flower.ai/) +This is an introductory example of **federated survival analysis** using [Flower](https://flower.ai/) and [lifelines](https://lifelines.readthedocs.io/en/stable/index.html) library. The aim of this example is to estimate the survival function using the @@ -25,86 +25,60 @@ the group it comes from therefore to simulate the division that might occur. Survival Function

-## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -$ git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/federated-kaplan-meier-fitter . && rm -rf _tmp && cd federated-kaplan-meier-fitter -``` - -This will create a new directory called `federated-kaplan-meier-fitter` containing the following files: - -```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- centralized.py --- README.md -``` - -### Installing Dependencies - -Project dependencies (such as `lifelines` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -#### Poetry - -```shell -poetry install -poetry shell -``` - -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +Start by cloning the example project: ```shell -poetry run python3 -c "import flwr" +$ git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/federated-kaplan-meier-fitter . && rm -rf _tmp && cd federated-kaplan-meier-fitter ``` -If you don't see any errors you're good to go! - -#### pip - -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +This will create a new directory called `federated-kaplan-meier-fitter` with the following structure: ```shell -pip install -r requirements.txt +federated-kaplan-meier-fitter +β”œβ”€β”€ examplefmk +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -## Run Federated Survival Analysis with Flower and lifelines's KaplanMeierFitter +### Install dependencies and project -### Start the long-running Flower server (SuperLink) +Install the dependencies defined in `pyproject.toml` as well as the `examplefmk` package. ```bash -flower-superlink --insecure +pip install -e . ``` -### Start the long-running Flower client (SuperNode) - -In a new terminal window, start the first long-running Flower client: +## Run the project -```bash -flower-client-app client:node_1_app --insecure -``` +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -In yet another new terminal window, start the second long-running Flower client: +### Run with the Simulation Engine ```bash -flower-client-app client:node_2_app --insecure +flwr run . ``` -### Run the Flower App - -With both the long-running server (SuperLink) and two clients (SuperNode) up and running, we can now run the actual Flower App: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flower-server-app server:app --insecure +flwr run . --run-config num-server-rounds=5,learning-rate=0.05 ``` -You will see that the server is printing survival function, median survival time and saves the plot with the survival function. - You can also check that the results match the centralized version. ```shell $ python3 centralized.py ``` + +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/federated-kaplan-meier-fitter/examplefkm/__init__.py b/examples/federated-kaplan-meier-fitter/examplefkm/__init__.py new file mode 100644 index 000000000000..794b6a6600e9 --- /dev/null +++ b/examples/federated-kaplan-meier-fitter/examplefkm/__init__.py @@ -0,0 +1 @@ +"""federated-kaplan-feier-fitter.""" diff --git a/examples/federated-kaplan-meier-fitter/client.py b/examples/federated-kaplan-meier-fitter/examplefkm/client_app.py similarity index 54% rename from examples/federated-kaplan-meier-fitter/client.py rename to examples/federated-kaplan-meier-fitter/examplefkm/client_app.py index 948492efc575..ea744af85be8 100644 --- a/examples/federated-kaplan-meier-fitter/client.py +++ b/examples/federated-kaplan-meier-fitter/examplefkm/client_app.py @@ -1,11 +1,13 @@ +"""examplefkm: A Flower / Lifelines app.""" + from typing import Dict, List, Tuple import flwr as fl import numpy as np -from datasets import Dataset -from flwr.common import NDArray, NDArrays -from flwr_datasets.partitioner import NaturalIdPartitioner -from lifelines.datasets import load_waltons +from flwr.client import Client, ClientApp +from flwr.common import NDArray, NDArrays, Context + +from examplefkm.task import load_partition class FlowerClient(fl.client.NumPyClient): @@ -40,26 +42,17 @@ def fit( ) -# Prepare data -X = load_waltons() -partitioner = NaturalIdPartitioner(partition_by="group") -partitioner.dataset = Dataset.from_pandas(X) - +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp. -def get_client_fn(partition_id: int): - def client_fn(cid: str): - partition = partitioner.load_partition(partition_id).to_pandas() - events = partition["E"].values - times = partition["T"].values - return FlowerClient(times=times, events=events).to_client() - - return client_fn + You can use settings in `context.run_config` to parameterize the + construction of your Client. You could use the `context.node_config` to, for + example, indicate which dataset to load (e.g accesing the partition-id). + """ + partition_id = context.node_config["partition-id"] + times, events = load_partition(partition_id) + return FlowerClient(times=times, events=events).to_client() -# Run via `flower-client-app client:app` -node_1_app = fl.client.ClientApp( - client_fn=get_client_fn(0), -) -node_2_app = fl.client.ClientApp( - client_fn=get_client_fn(1), -) +# Flower ClientApp +app = ClientApp(client_fn=client_fn) diff --git a/examples/federated-kaplan-meier-fitter/server.py b/examples/federated-kaplan-meier-fitter/examplefkm/server_app.py similarity index 83% rename from examples/federated-kaplan-meier-fitter/server.py rename to examples/federated-kaplan-meier-fitter/examplefkm/server_app.py index e1f84a961bf1..2515e8ea852d 100644 --- a/examples/federated-kaplan-meier-fitter/server.py +++ b/examples/federated-kaplan-meier-fitter/examplefkm/server_app.py @@ -16,8 +16,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union -import flwr as fl -import matplotlib.pyplot as plt import numpy as np from flwr.common import ( EvaluateIns, @@ -27,7 +25,9 @@ Parameters, Scalar, parameters_to_ndarrays, + Context, ) +from flwr.server import ServerApp, ServerConfig, ServerAppComponents from flwr.server.client_manager import ClientManager from flwr.server.client_proxy import ClientProxy from flwr.server.strategy import Strategy @@ -66,7 +66,7 @@ def configure_fit( config = {} fit_ins = FitIns(parameters, config) clients = client_manager.sample( - num_clients=client_manager.num_available(), + num_clients=self._min_num_clients, min_num_clients=self._min_num_clients, ) return [(client, fit_ins) for client in clients] @@ -99,9 +99,6 @@ def aggregate_fit( self.fitter.fit(sorted_times, sorted_events) print("Survival function:") print(self.fitter.survival_function_) - self.fitter.plot_survival_function() - plt.title("Survival function of fruit flies (Walton's data)", fontsize=16) - plt.savefig("./_static/survival_function_federated.png", dpi=200) print("Mean survival time:") print(self.fitter.median_survival_time_) return None, {} @@ -136,10 +133,25 @@ def configure_evaluate( return [] -fitter = KaplanMeierFitter() # You can choose other method that work on E, T data -strategy = EventTimeFitterStrategy(min_num_clients=2, fitter=fitter) +def server_fn(context: Context) -> ServerAppComponents: + """Construct components that set the ServerApp behaviour. -app = fl.server.ServerApp( - config=fl.server.ServerConfig(num_rounds=1), - strategy=strategy, -) + You can use settings in `context.run_config` to parameterize the + construction of all elements (e.g the strategy or the number of rounds) + wrapped in the returned ServerAppComponents object. + """ + + # Define the strategy + fitter = KaplanMeierFitter() # You can choose other method that work on E, T data + min_num_clients = context.run_config["min-num-clients"] + strategy = EventTimeFitterStrategy(min_num_clients=min_num_clients, fitter=fitter) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/federated-kaplan-meier-fitter/examplefkm/task.py b/examples/federated-kaplan-meier-fitter/examplefkm/task.py new file mode 100644 index 000000000000..d76dc79c6724 --- /dev/null +++ b/examples/federated-kaplan-meier-fitter/examplefkm/task.py @@ -0,0 +1,17 @@ +"""examplefkm: A Flower / Lifelines app.""" + +from lifelines.datasets import load_waltons + +from flwr_datasets.partitioner import NaturalIdPartitioner +from datasets import Dataset + +X = load_waltons() + + +def load_partition(partition_id: int): + partitioner = NaturalIdPartitioner(partition_by="group") + partitioner.dataset = Dataset.from_pandas(X) + partition = partitioner.load_partition(partition_id).to_pandas() + times = partition["T"].values + events = partition["E"].values + return times, events diff --git a/examples/federated-kaplan-meier-fitter/pyproject.toml b/examples/federated-kaplan-meier-fitter/pyproject.toml index 8fe354ffb750..47cb0a4ba286 100644 --- a/examples/federated-kaplan-meier-fitter/pyproject.toml +++ b/examples/federated-kaplan-meier-fitter/pyproject.toml @@ -1,18 +1,35 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] +[project] name = "federated-kaplan-meier-fitter" -version = "0.1.0" +version = "1.0.0" description = "Federated Kaplan Meier Fitter with Flower" -authors = ["The Flower Authors "] -maintainers = ["The Flower Authors "] +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.10.0", + "flwr-datasets>=0.3.0", + "numpy>=1.23.2", + "pandas>=2.0.0", + "lifelines>=0.28.0", +] +[tool.hatch.build.targets.wheel] +packages = ["."] -[tool.poetry.dependencies] -python = ">=3.9,<3.11" -flwr-nightly = "*" -flwr-datasets = ">=0.0.2,<1.0.0" -numpy = ">=1.23.2" -pandas = ">=2.0.0" -lifelines = ">=0.28.0" +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "examplefkm.server_app:app" +clientapp = "examplefkm.client_app:app" + +[tool.flwr.app.config] +min-num-clients = 2 +num-server-rounds = 1 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 2 diff --git a/examples/federated-kaplan-meier-fitter/requirements.txt b/examples/federated-kaplan-meier-fitter/requirements.txt deleted file mode 100644 index cc8146545c7b..000000000000 --- a/examples/federated-kaplan-meier-fitter/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flwr-nightly -flwr-datasets>=0.0.2, <1.0.0 -numpy>=1.23.2 -pandas>=2.0.0 -lifelines>=0.28.0 From 71c4eed28d31a17c19c863917913a666d44fd6e9 Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 24 Aug 2024 14:31:52 +0100 Subject: [PATCH 125/188] docs(framework) Add note to `flower-server-app` CLI ref (#4076) --- doc/source/ref-api-cli.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/source/ref-api-cli.rst b/doc/source/ref-api-cli.rst index ff1a9606f58d..95664b2f490a 100644 --- a/doc/source/ref-api-cli.rst +++ b/doc/source/ref-api-cli.rst @@ -45,6 +45,12 @@ flower-supernode flower-server-app ~~~~~~~~~~~~~~~~~ +.. note:: + Note that since version :code:`1.11.0`, :code:`flower-server-app` no longer supports passing a reference to a `ServerApp` attribute. + Instead, you need to pass the path to Flower app via the argument :code:`--app`. + This is the path to a directory containing a `pyproject.toml`. + You can create a valid Flower app by executing :code:`flwr new` and following the prompt. + .. argparse:: :module: flwr.server.run_serverapp :func: _parse_args_run_server_app From 5be5b1d09393db2937d6ff94e9ca874a15c1bc04 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Sat, 24 Aug 2024 14:37:26 +0100 Subject: [PATCH 126/188] feat(framework:skip) Print prompts when users pass a reference instead of a path to `flower-server-app` (#4077) --- src/py/flwr/server/run_serverapp.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/server/run_serverapp.py b/src/py/flwr/server/run_serverapp.py index 8f67c917c8ed..d9c363245a2e 100644 --- a/src/py/flwr/server/run_serverapp.py +++ b/src/py/flwr/server/run_serverapp.py @@ -97,6 +97,21 @@ def run_server_app() -> None: args = _parse_args_run_server_app().parse_args() + # Check if the server app reference is passed. + # Since Flower 1.11, passing a reference is not allowed. + app_path: Optional[str] = args.app + # If the provided app_path doesn't exist, and contains a ":", + # it is likely to be a server app reference instead of a path. + if app_path is not None and not Path(app_path).exists() and ":" in app_path: + sys.exit( + "It appears you've passed a reference like `server:app`.\n\n" + "Note that since version `1.11.0`, `flower-server-app` no longer supports " + "passing a reference to a `ServerApp` attribute. Instead, you need to pass " + "the path to Flower app via the argument `--app`. This is the path to a " + "directory containing a `pyproject.toml`. You can create a valid Flower " + "app by executing `flwr new` and following the prompt." + ) + if args.server != ADDRESS_DRIVER_API: warn = "Passing flag --server is deprecated. Use --superlink instead." warn_deprecated_feature(warn) @@ -151,7 +166,6 @@ def run_server_app() -> None: cert_path, ) - app_path: Optional[str] = args.app if not (app_path is None) ^ (args.run_id is None): raise sys.exit( "Please provide either a Flower App path or a Run ID, but not both. " From dbe957018153074ae412e99307550e9752a602b0 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Sat, 24 Aug 2024 17:17:40 +0100 Subject: [PATCH 127/188] refactor(examples) Update Flower example using Hugging Face (#3754) Co-authored-by: jafermarq --- examples/quickstart-huggingface/README.md | 83 ++++++----- examples/quickstart-huggingface/client.py | 129 ------------------ .../huggingface_example/__init__.py | 1 + .../huggingface_example/client_app.py | 58 ++++++++ .../huggingface_example/server_app.py | 33 +++++ .../huggingface_example/task.py | 105 ++++++++++++++ .../quickstart-huggingface/pyproject.toml | 61 ++++++--- .../quickstart-huggingface/requirements.txt | 7 - examples/quickstart-huggingface/run.sh | 15 -- examples/quickstart-huggingface/server.py | 15 -- 10 files changed, 282 insertions(+), 225 deletions(-) delete mode 100644 examples/quickstart-huggingface/client.py create mode 100644 examples/quickstart-huggingface/huggingface_example/__init__.py create mode 100644 examples/quickstart-huggingface/huggingface_example/client_app.py create mode 100644 examples/quickstart-huggingface/huggingface_example/server_app.py create mode 100644 examples/quickstart-huggingface/huggingface_example/task.py delete mode 100644 examples/quickstart-huggingface/requirements.txt delete mode 100755 examples/quickstart-huggingface/run.sh delete mode 100644 examples/quickstart-huggingface/server.py diff --git a/examples/quickstart-huggingface/README.md b/examples/quickstart-huggingface/README.md index fa4330040ea7..ac0acebb9b99 100644 --- a/examples/quickstart-huggingface/README.md +++ b/examples/quickstart-huggingface/README.md @@ -4,77 +4,76 @@ dataset: [IMDB] framework: [transformers] --- -# Federated HuggingFace Transformers using Flower and PyTorch +# Federated Learning with HuggingFace Transformers and Flower (Quickstart Example) -This introductory example to using [HuggingFace](https://huggingface.co) Transformers with Flower with PyTorch. This example has been extended from the [quickstart-pytorch](https://flower.ai/docs/examples/quickstart-pytorch.html) example. The training script closely follows the [HuggingFace course](https://huggingface.co/course/chapter3?fw=pt), so you are encouraged to check that out for a detailed explanation of the transformer pipeline. +This introductory example to using [πŸ€—Transformers](https://huggingface.co/docs/transformers/en/index) with Flower. The training script closely follows the [HuggingFace course](https://huggingface.co/course/chapter3?fw=pt), so you are encouraged to check that out for a detailed explanation of the transformer pipeline. -Like `quickstart-pytorch`, running this example in itself is also meant to be quite easy. +In this example, we will federated the training of a [DistilBERT](https://huggingface.co/distilbert/distilbert-base-uncased) modle on the [IMDB](https://huggingface.co/datasets/stanfordnlp/imdb) dataset. The data will be downloaded and partitioned using [Flower Datasets](https://flower.ai/docs/datasets/). This example runs best when a GPU is available. -## Project Setup +## Set up the project + +### Clone the project Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: ```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/quickstart-huggingface . && rm -rf flower && cd quickstart-huggingface +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-huggingface . \ + && rm -rf _tmp && cd quickstart-huggingface ``` This will create a new directory called `quickstart-huggingface` containing the following files: ```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- README.md +quickstart-huggingface +β”œβ”€β”€ huggingface_example +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies - -Project dependencies (such as `torch` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +### Install dependencies and project -#### Poetry +Install the dependencies defined in `pyproject.toml` as well as the `huggingface_example` package. -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: +## Run the Example -```shell -poetry run python3 -c "import flwr" -``` - -If you don't see any errors you're good to go! +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -#### pip +### Run with the Simulation Engine -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +> \[!TIP\] +> This example runs faster when the `ClientApp`s have access to a GPU. If your system has one, you can make use of it by configuring the `backend.client-resources` component in `pyproject.toml`. If you want to try running the example with GPU right away, use the `local-simulation-gpu` federation as shown below. -```shell -pip install -r requirements.txt +```bash +# Run with the default federation (CPU only) +flwr run . ``` -## Run Federated Learning with Flower +Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 1x`ClientApp` (using ~12 GB of VRAM) will run in parallel in each available GPU. Note you can adjust the degree of paralellism but modifying the `client-resources` specification. -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: - -```shell -python3 server.py +```bash +# Run with the `local-simulation-gpu` federation +flwr run . local-simulation-gpu ``` -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminal windows and run the following commands. - -Start client 1 in the first terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example -```shell -python3 client.py --partition-id 0 +```bash +flwr run --run-config num-server-rounds=5 ``` -Start client 2 in the second terminal: +> \[!TIP\] +> For a more detailed walk-through check our [quickstart πŸ€—Transformers tutorial](https://flower.ai/docs/framework/tutorial-quickstart-huggingface.html) -```shell -python3 client.py --partition-id 1 -``` +### Run with the Deployment Engine -You will see that PyTorch is starting a federated training. +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-huggingface/client.py b/examples/quickstart-huggingface/client.py deleted file mode 100644 index b880119d1c7c..000000000000 --- a/examples/quickstart-huggingface/client.py +++ /dev/null @@ -1,129 +0,0 @@ -import argparse -import warnings -from collections import OrderedDict - -import flwr as fl -import torch -from evaluate import load as load_metric -from flwr_datasets import FederatedDataset -from torch.optim import AdamW -from torch.utils.data import DataLoader -from transformers import ( - AutoModelForSequenceClassification, - AutoTokenizer, - DataCollatorWithPadding, -) - -warnings.filterwarnings("ignore", category=UserWarning) -DEVICE = torch.device("cpu") -CHECKPOINT = "distilbert-base-uncased" # transformer model checkpoint - - -def load_data(partition_id): - """Load IMDB data (training and eval)""" - fds = FederatedDataset(dataset="imdb", partitioners={"train": 1_000}) - partition = fds.load_partition(partition_id) - # Divide data: 80% train, 20% test - partition_train_test = partition.train_test_split(test_size=0.2, seed=42) - - tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT, model_max_length=512) - - def tokenize_function(examples): - return tokenizer(examples["text"], truncation=True) - - partition_train_test = partition_train_test.map(tokenize_function, batched=True) - partition_train_test = partition_train_test.remove_columns("text") - partition_train_test = partition_train_test.rename_column("label", "labels") - - data_collator = DataCollatorWithPadding(tokenizer=tokenizer) - trainloader = DataLoader( - partition_train_test["train"], - shuffle=True, - batch_size=32, - collate_fn=data_collator, - ) - - testloader = DataLoader( - partition_train_test["test"], batch_size=32, collate_fn=data_collator - ) - - return trainloader, testloader - - -def train(net, trainloader, epochs): - optimizer = AdamW(net.parameters(), lr=5e-5) - net.train() - for _ in range(epochs): - for batch in trainloader: - batch = {k: v.to(DEVICE) for k, v in batch.items()} - outputs = net(**batch) - loss = outputs.loss - loss.backward() - optimizer.step() - optimizer.zero_grad() - - -def test(net, testloader): - metric = load_metric("accuracy") - loss = 0 - net.eval() - for batch in testloader: - batch = {k: v.to(DEVICE) for k, v in batch.items()} - with torch.no_grad(): - outputs = net(**batch) - logits = outputs.logits - loss += outputs.loss.item() - predictions = torch.argmax(logits, dim=-1) - metric.add_batch(predictions=predictions, references=batch["labels"]) - loss /= len(testloader.dataset) - accuracy = metric.compute()["accuracy"] - return loss, accuracy - - -def main(partition_id): - net = AutoModelForSequenceClassification.from_pretrained( - CHECKPOINT, num_labels=2 - ).to(DEVICE) - - trainloader, testloader = load_data(partition_id) - - # Flower client - class IMDBClient(fl.client.NumPyClient): - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - print("Training Started...") - train(net, trainloader, epochs=1) - print("Training Finished.") - return self.get_parameters(config={}), len(trainloader), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, accuracy = test(net, testloader) - return float(loss), len(testloader), {"accuracy": float(accuracy)} - - # Start client - fl.client.start_client( - server_address="127.0.0.1:8080", client=IMDBClient().to_client() - ) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--partition-id", - choices=list(range(1_000)), - required=True, - type=int, - help="Partition of the dataset divided into 1,000 iid partitions created " - "artificially.", - ) - partition_id = parser.parse_args().partition_id - main(partition_id) diff --git a/examples/quickstart-huggingface/huggingface_example/__init__.py b/examples/quickstart-huggingface/huggingface_example/__init__.py new file mode 100644 index 000000000000..6d897650c6bf --- /dev/null +++ b/examples/quickstart-huggingface/huggingface_example/__init__.py @@ -0,0 +1 @@ +"""huggingface_example: A Flower / Hugging Face app.""" diff --git a/examples/quickstart-huggingface/huggingface_example/client_app.py b/examples/quickstart-huggingface/huggingface_example/client_app.py new file mode 100644 index 000000000000..8989e52281ad --- /dev/null +++ b/examples/quickstart-huggingface/huggingface_example/client_app.py @@ -0,0 +1,58 @@ +"""huggingface_example: A Flower / Hugging Face app.""" + +import warnings + +import torch +from flwr.client import Client, ClientApp, NumPyClient +from flwr.common import Context +from transformers import logging +from huggingface_example.task import ( + train, + test, + load_data, + set_params, + get_params, + get_model, +) + +warnings.filterwarnings("ignore", category=FutureWarning) + +# To mute warnings reminding that we need to train the model to a downstream task +# This is something this example does. +logging.set_verbosity_error() + + +# Flower client +class IMDBClient(NumPyClient): + def __init__(self, model_name, trainloader, testloader) -> None: + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.trainloader = trainloader + self.testloader = testloader + self.net = get_model(model_name) + self.net.to(self.device) + + def fit(self, parameters, config) -> tuple[list, int, dict]: + set_params(self.net, parameters) + train(self.net, self.trainloader, epochs=1, device=self.device) + return get_params(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config) -> tuple[float, int, dict[str, float]]: + set_params(self.net, parameters) + loss, accuracy = test(self.net, self.testloader, device=self.device) + return float(loss), len(self.testloader), {"accuracy": float(accuracy)} + + +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp.""" + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + # Read the run config to get settings to configure the Client + model_name = context.run_config["model-name"] + trainloader, testloader = load_data(partition_id, num_partitions, model_name) + + return IMDBClient(model_name, trainloader, testloader).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-huggingface/huggingface_example/server_app.py b/examples/quickstart-huggingface/huggingface_example/server_app.py new file mode 100644 index 000000000000..d0db1b43fa36 --- /dev/null +++ b/examples/quickstart-huggingface/huggingface_example/server_app.py @@ -0,0 +1,33 @@ +"""huggingface_example: A Flower / Hugging Face app.""" + +from flwr.common import Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from huggingface_example.task import get_params, get_model + + +def server_fn(context: Context) -> ServerAppComponents: + """Construct components for ServerApp.""" + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + # Set global model initialization + model_name = context.run_config["model-name"] + ndarrays = get_params(get_model(model_name)) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define strategy + fraction_fit = context.run_config["fraction-fit"] + fraction_evaluate = context.run_config["fraction-evaluate"] + strategy = FedAvg( + fraction_fit=fraction_fit, + fraction_evaluate=fraction_evaluate, + initial_parameters=global_model_init, + ) + + return ServerAppComponents(config=config, strategy=strategy) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-huggingface/huggingface_example/task.py b/examples/quickstart-huggingface/huggingface_example/task.py new file mode 100644 index 000000000000..25304d134a67 --- /dev/null +++ b/examples/quickstart-huggingface/huggingface_example/task.py @@ -0,0 +1,105 @@ +"""huggingface_example: A Flower / Hugging Face app.""" + +from typing import Any +from collections import OrderedDict + +import torch +from evaluate import load as load_metric +from torch.optim import AdamW +from torch.utils.data import DataLoader +from transformers import ( + AutoTokenizer, + DataCollatorWithPadding, + AutoModelForSequenceClassification, +) +from datasets.utils.logging import disable_progress_bar +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + + +disable_progress_bar() +fds = None # Cache FederatedDataset + + +def load_data( + partition_id: int, num_partitions: int, model_name: str +) -> tuple[DataLoader[Any], DataLoader[Any]]: + """Load IMDB data (training and eval)""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + # Partition the IMDB dataset into N partitions + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="stanfordnlp/imdb", partitioners={"train": partitioner} + ) + partition = fds.load_partition(partition_id) + # Divide data: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + + tokenizer = AutoTokenizer.from_pretrained(model_name, model_max_length=512) + + def tokenize_function(examples): + return tokenizer(examples["text"], truncation=True) + + partition_train_test = partition_train_test.map(tokenize_function, batched=True) + partition_train_test = partition_train_test.remove_columns("text") + partition_train_test = partition_train_test.rename_column("label", "labels") + + data_collator = DataCollatorWithPadding(tokenizer=tokenizer) + trainloader = DataLoader( + partition_train_test["train"], + shuffle=True, + batch_size=32, + collate_fn=data_collator, + ) + + testloader = DataLoader( + partition_train_test["test"], batch_size=32, collate_fn=data_collator + ) + + return trainloader, testloader + + +def get_model(model_name): + return AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) + + +def get_params(model): + return [val.cpu().numpy() for _, val in model.state_dict().items()] + + +def set_params(model, parameters) -> None: + params_dict = zip(model.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.Tensor(v) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) + + +def train(net, trainloader, epochs, device) -> None: + optimizer = AdamW(net.parameters(), lr=5e-5) + net.train() + for _ in range(epochs): + for batch in trainloader: + batch = {k: v.to(device) for k, v in batch.items()} + outputs = net(**batch) + loss = outputs.loss + loss.backward() + optimizer.step() + optimizer.zero_grad() + + +def test(net, testloader, device) -> tuple[Any | float, Any]: + metric = load_metric("accuracy") + loss = 0 + net.eval() + for batch in testloader: + batch = {k: v.to(device) for k, v in batch.items()} + with torch.no_grad(): + outputs = net(**batch) + logits = outputs.logits + loss += outputs.loss.item() + predictions = torch.argmax(logits, dim=-1) + metric.add_batch(predictions=predictions, references=batch["labels"]) + loss /= len(testloader.dataset) + accuracy = metric.compute()["accuracy"] + return loss, accuracy diff --git a/examples/quickstart-huggingface/pyproject.toml b/examples/quickstart-huggingface/pyproject.toml index 2b46804d7b45..af48b2429635 100644 --- a/examples/quickstart-huggingface/pyproject.toml +++ b/examples/quickstart-huggingface/pyproject.toml @@ -1,22 +1,49 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "quickstart-huggingface" -version = "0.1.0" -description = "Hugging Face Transformers Federated Learning Quickstart with Flower" +[project] +name = "huggingface_example" +version = "1.0.0" +description = "Federated Learning with Hugginface Transformers and Flower (Quickstart Example)" +license = "Apache-2.0" authors = [ - "The Flower Authors ", - "Kaushik Amar Das ", + { name = "The Flower Authors", email = "hello@flower.ai" }, + { name = "Kaushik Amar Das", email = "kaushik.das@iiitg.ac.in" }, ] +dependencies = [ + "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr-datasets>=0.3.0", + "torch==2.4.0", + "transformers>=4.30.0,<5.0", + "evaluate>=0.4.0,<1.0", + "datasets>=2.0.0, <3.0", + "scikit-learn>=1.3.1, <2.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "huggingface_example.server_app:app" +clientapp = "huggingface_example.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +model-name = "distilbert-base-uncased" +fraction-fit = 0.05 +fraction-evaluate = 0.1 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 100 -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -flwr-datasets = ">=0.0.2,<1.0.0" -torch = ">=1.13.1,<2.0" -transformers = ">=4.30.0,<5.0" -evaluate = ">=0.4.0,<1.0" -datasets = ">=2.0.0, <3.0" -scikit-learn = ">=1.3.1, <2.0" +[tool.flwr.federations.local-simulation-gpu] +options.num-supernodes = 100 +options.backend.client-resources.num-cpus = 4 # each ClientApp assumes to use 4CPUs +options.backend.client-resources.num-gpus = 1.0 # at most 1 ClientApp will run in a given GPU (lower it to increase parallelism) \ No newline at end of file diff --git a/examples/quickstart-huggingface/requirements.txt b/examples/quickstart-huggingface/requirements.txt deleted file mode 100644 index 3cd5735625ba..000000000000 --- a/examples/quickstart-huggingface/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -flwr>=1.0, <2.0 -flwr-datasets>=0.0.2, <1.0.0 -torch>=1.13.1, <2.0 -transformers>=4.30.0, <5.0 -evaluate>=0.4.0, <1.0 -datasets>=2.0.0, <3.0 -scikit-learn>=1.3.1, <2.0 diff --git a/examples/quickstart-huggingface/run.sh b/examples/quickstart-huggingface/run.sh deleted file mode 100755 index fa989eab1471..000000000000 --- a/examples/quickstart-huggingface/run.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python client.py --partition-id ${i}& -done - -# This will allow you to use CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-huggingface/server.py b/examples/quickstart-huggingface/server.py deleted file mode 100644 index 4eeb9da7da75..000000000000 --- a/examples/quickstart-huggingface/server.py +++ /dev/null @@ -1,15 +0,0 @@ -import flwr as fl - -if __name__ == "__main__": - # Define strategy - strategy = fl.server.strategy.FedAvg( - fraction_fit=1.0, - fraction_evaluate=1.0, - ) - - # Start server - fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, - ) From ecac7f54e659a3cad23f482bdc62f002db35ba4d Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 24 Aug 2024 17:24:48 +0100 Subject: [PATCH 128/188] refactor(examples) Update `quickstart-monai` example (#3934) --- examples/quickstart-monai/.gitignore | 1 + examples/quickstart-monai/README.md | 96 ++++----- examples/quickstart-monai/client.py | 61 ------ examples/quickstart-monai/data.py | 158 -------------- examples/quickstart-monai/model.py | 33 --- .../quickstart-monai/monaiexample/__init__.py | 0 .../monaiexample/client_app.py | 41 ++++ .../monaiexample/server_app.py | 46 ++++ .../quickstart-monai/monaiexample/task.py | 199 ++++++++++++++++++ examples/quickstart-monai/pyproject.toml | 58 +++-- examples/quickstart-monai/requirements.txt | 7 - examples/quickstart-monai/run.sh | 19 -- examples/quickstart-monai/server.py | 25 --- 13 files changed, 369 insertions(+), 375 deletions(-) delete mode 100644 examples/quickstart-monai/client.py delete mode 100644 examples/quickstart-monai/data.py delete mode 100644 examples/quickstart-monai/model.py create mode 100644 examples/quickstart-monai/monaiexample/__init__.py create mode 100644 examples/quickstart-monai/monaiexample/client_app.py create mode 100644 examples/quickstart-monai/monaiexample/server_app.py create mode 100644 examples/quickstart-monai/monaiexample/task.py delete mode 100644 examples/quickstart-monai/requirements.txt delete mode 100755 examples/quickstart-monai/run.sh delete mode 100644 examples/quickstart-monai/server.py diff --git a/examples/quickstart-monai/.gitignore b/examples/quickstart-monai/.gitignore index a218cab9669e..2626387e2a4f 100644 --- a/examples/quickstart-monai/.gitignore +++ b/examples/quickstart-monai/.gitignore @@ -1 +1,2 @@ MedNIST* +.data_download.lock diff --git a/examples/quickstart-monai/README.md b/examples/quickstart-monai/README.md index dc31f03e4b1b..c470a6a6c86f 100644 --- a/examples/quickstart-monai/README.md +++ b/examples/quickstart-monai/README.md @@ -4,88 +4,76 @@ dataset: [MedNIST] framework: [MONAI] --- -# Flower Example using MONAI +# Federated Learning with MONAI and Flower (Quickstart Example) This introductory example to Flower uses MONAI, but deep knowledge of MONAI is not necessarily required to run the example. However, it will help you understand how to adapt Flower to your use case. -Running this example in itself is quite easy. +Running this example in itself is quite easy. [MONAI](https://docs.monai.io/en/latest/index.html)(Medical Open Network for AI) is a PyTorch-based, open-source framework for deep learning in healthcare imaging, part of the PyTorch Ecosystem. This example uses a subset of the [MedMNIST](https://medmnist.com/) dataset including 6 classes, as done in [MONAI's classification demo](https://colab.research.google.com/drive/1wy8XUSnNWlhDNazFdvGBHLfdkGvOHBKe). Each client trains am [DenseNet121](https://docs.monai.io/en/stable/networks.html#densenet121) from MONAI. -[MONAI](https://docs.monai.io/en/latest/index.html)(Medical Open Network for AI) is a PyTorch-based, open-source framework for deep learning in healthcare imaging, part of the PyTorch Ecosystem. +> \[!NOTE\] +> This example uses [Flower Datasets](https://flower.ai/docs/datasets/) to partition the MedMNIST dataset. Its a good example to show how to bring any dataset into Flower and partition it using any of the built-in [partitioners](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.html) (e.g. `DirichletPartitioner`, `PathologicalPartitioner`). Learn [how to use partitioners](https://flower.ai/docs/datasets/tutorial-use-partitioners.html) in a step-by-step tutorial. -Its ambitions are: +## Set up the project -- developing a community of academic, industrial and clinical researchers collaborating on a common foundation; +### Clone the project -- creating state-of-the-art, end-to-end training workflows for healthcare imaging; - -- providing researchers with an optimized and standardized way to create and evaluate deep learning models. - -## Project Setup - -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +Start by cloning the example project: ```shell -git clone --depth=1 https://github.com/adap/flower.git _tmp && mv _tmp/examples/quickstart-monai . && rm -rf _tmp && cd quickstart-monai +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/quickstart-monai . \ + && rm -rf _tmp \ + && cd quickstart-monai ``` -This will create a new directory called `quickstart-monai` containing the following files: +This will create a new directory called `quickstart-monai` with the following structure: ```shell --- pyproject.toml --- requirements.txt --- client.py --- data.py --- model.py --- server.py --- README.md +quickstart-monai +β”œβ”€β”€ monaiexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -### Installing Dependencies - -Project dependencies (such as `monai` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +### Install dependencies and project -#### Poetry +Install the dependencies defined in `pyproject.toml` as well as the `monaiexample` package. -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python3 -c "import flwr" -``` +## Run the project -If you don't see any errors you're good to go! +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -#### pip +### Run with the Simulation Engine -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +> \[!TIP\] +> This example runs faster when the `ClientApp`s have access to a GPU. If your system has one, you can make use of it by configuring the `backend.client-resources` component in `pyproject.toml`. If you want to try running the example with GPU right away, use the `local-simulation-gpu` federation as shown below. -```shell -pip install -r requirements.txt +```bash +# Run with the default federation (CPU only) +flwr run . ``` -## Run Federated Learning with MONAI and Flower - -Afterwards you are ready to start the Flower server as well as the clients. You can simply start the server in a terminal as follows: +Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 4x`ClientApp` will run in parallel in the available GPU. -```shell -python3 server.py +```bash +# Run with the `local-simulation-gpu` federation +flwr run . local-simulation-gpu ``` -Now you are ready to start the Flower clients which will participate in the learning. To do so simply open two more terminal windows and run the following commands. Clients will train a [DenseNet121](https://docs.monai.io/en/stable/networks.html#densenet121) from MONAI. If a GPU is present in your system, clients will use it. - -Start client 1 in the first terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 client.py --partition-id 0 +```bash +flwr run . --run-config num-server-rounds=5,batch-size=32 ``` -Start client 2 in the second terminal: - -```shell -python3 client.py --partition-id 1 -``` +### Run with the Deployment Engine -You will see that the federated training is starting. Look at the [code](https://github.com/adap/flower/tree/main/examples/quickstart-monai) for a detailed explanation. +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/quickstart-monai/client.py b/examples/quickstart-monai/client.py deleted file mode 100644 index 1401928af1ff..000000000000 --- a/examples/quickstart-monai/client.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse -import warnings -from collections import OrderedDict - -import flwr as fl -import torch -from monai.networks.nets.densenet import DenseNet121 - -from data import load_data -from model import test, train - -warnings.filterwarnings("ignore", category=UserWarning) -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -# Define Flower client -class FlowerClient(fl.client.NumPyClient): - def __init__(self, net, trainloader, testloader, device): - self.net = net - self.trainloader = trainloader - self.testloader = testloader - self.device = device - - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in self.net.state_dict().items()] - - def set_parameters(self, parameters): - params_dict = zip(self.net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - self.net.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - self.set_parameters(parameters) - train(self.net, self.trainloader, epoch_num=1, device=self.device) - return self.get_parameters(config={}), len(self.trainloader), {} - - def evaluate(self, parameters, config): - self.set_parameters(parameters) - loss, accuracy = test(self.net, self.testloader, self.device) - return loss, len(self.testloader), {"accuracy": accuracy} - - -if __name__ == "__main__": - total_partitions = 10 - parser = argparse.ArgumentParser() - parser.add_argument( - "--partition-id", type=int, choices=range(total_partitions), required=True - ) - args = parser.parse_args() - - # Load model and data (simple CNN, CIFAR-10) - trainloader, _, testloader, num_class = load_data( - total_partitions, args.partition_id - ) - net = DenseNet121(spatial_dims=2, in_channels=1, out_channels=num_class).to(DEVICE) - - # Start Flower client - fl.client.start_numpy_client( - server_address="127.0.0.1:8080", - client=FlowerClient(net, trainloader, testloader, DEVICE), - ) diff --git a/examples/quickstart-monai/data.py b/examples/quickstart-monai/data.py deleted file mode 100644 index d184476522e8..000000000000 --- a/examples/quickstart-monai/data.py +++ /dev/null @@ -1,158 +0,0 @@ -import os -import tarfile -from urllib import request - -import numpy as np -from monai.data import DataLoader, Dataset -from monai.transforms import ( - Compose, - EnsureChannelFirst, - LoadImage, - RandFlip, - RandRotate, - RandZoom, - ScaleIntensity, - ToTensor, -) - - -def _partition(files_list, labels_list, num_shards, index): - total_size = len(files_list) - assert total_size == len( - labels_list - ), f"List of datapoints and labels must be of the same length" - shard_size = total_size // num_shards - - # Calculate start and end indices for the shard - start_idx = index * shard_size - if index == num_shards - 1: - # Last shard takes the remainder - end_idx = total_size - else: - end_idx = start_idx + shard_size - - # Create a subset for the shard - files = files_list[start_idx:end_idx] - labels = labels_list[start_idx:end_idx] - return files, labels - - -def load_data(num_shards, index): - image_file_list, image_label_list, _, num_class = _download_data() - - # Get partition given index - files_list, labels_list = _partition( - image_file_list, image_label_list, num_shards, index - ) - - trainX, trainY, valX, valY, testX, testY = _split_data( - files_list, labels_list, len(files_list) - ) - train_transforms, val_transforms = _get_transforms() - - train_ds = MedNISTDataset(trainX, trainY, train_transforms) - train_loader = DataLoader(train_ds, batch_size=300, shuffle=True) - - val_ds = MedNISTDataset(valX, valY, val_transforms) - val_loader = DataLoader(val_ds, batch_size=300) - - test_ds = MedNISTDataset(testX, testY, val_transforms) - test_loader = DataLoader(test_ds, batch_size=300) - - return train_loader, val_loader, test_loader, num_class - - -class MedNISTDataset(Dataset): - def __init__(self, image_files, labels, transforms): - self.image_files = image_files - self.labels = labels - self.transforms = transforms - - def __len__(self): - return len(self.image_files) - - def __getitem__(self, index): - return self.transforms(self.image_files[index]), self.labels[index] - - -def _download_data(): - data_dir = "./MedNIST/" - _download_and_extract( - "https://dl.dropboxusercontent.com/s/5wwskxctvcxiuea/MedNIST.tar.gz", - os.path.join(data_dir), - ) - - class_names = sorted( - [x for x in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, x))] - ) - num_class = len(class_names) - image_files = [ - [ - os.path.join(data_dir, class_name, x) - for x in os.listdir(os.path.join(data_dir, class_name)) - ] - for class_name in class_names - ] - image_file_list = [] - image_label_list = [] - for i, class_name in enumerate(class_names): - image_file_list.extend(image_files[i]) - image_label_list.extend([i] * len(image_files[i])) - num_total = len(image_label_list) - return image_file_list, image_label_list, num_total, num_class - - -def _split_data(image_file_list, image_label_list, num_total): - valid_frac, test_frac = 0.1, 0.1 - trainX, trainY = [], [] - valX, valY = [], [] - testX, testY = [], [] - - for i in range(num_total): - rann = np.random.random() - if rann < valid_frac: - valX.append(image_file_list[i]) - valY.append(image_label_list[i]) - elif rann < test_frac + valid_frac: - testX.append(image_file_list[i]) - testY.append(image_label_list[i]) - else: - trainX.append(image_file_list[i]) - trainY.append(image_label_list[i]) - - return trainX, trainY, valX, valY, testX, testY - - -def _get_transforms(): - train_transforms = Compose( - [ - LoadImage(image_only=True), - EnsureChannelFirst(), - ScaleIntensity(), - RandRotate(range_x=15, prob=0.5, keep_size=True), - RandFlip(spatial_axis=0, prob=0.5), - RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5, keep_size=True), - ToTensor(), - ] - ) - - val_transforms = Compose( - [LoadImage(image_only=True), EnsureChannelFirst(), ScaleIntensity(), ToTensor()] - ) - - return train_transforms, val_transforms - - -def _download_and_extract(url, dest_folder): - if not os.path.isdir(dest_folder): - # Download the tar.gz file - tar_gz_filename = url.split("/")[-1] - if not os.path.isfile(tar_gz_filename): - with request.urlopen(url) as response, open( - tar_gz_filename, "wb" - ) as out_file: - out_file.write(response.read()) - - # Extract the tar.gz file - with tarfile.open(tar_gz_filename, "r:gz") as tar_ref: - tar_ref.extractall() diff --git a/examples/quickstart-monai/model.py b/examples/quickstart-monai/model.py deleted file mode 100644 index 4c74d50553e4..000000000000 --- a/examples/quickstart-monai/model.py +++ /dev/null @@ -1,33 +0,0 @@ -import torch - - -def train(model, train_loader, epoch_num, device): - loss_function = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.Adam(model.parameters(), 1e-5) - for _ in range(epoch_num): - model.train() - for inputs, labels in train_loader: - optimizer.zero_grad() - loss_function(model(inputs.to(device)), labels.to(device)).backward() - optimizer.step() - - -def test(model, test_loader, device): - model.eval() - loss = 0.0 - y_true = list() - y_pred = list() - loss_function = torch.nn.CrossEntropyLoss() - with torch.no_grad(): - for test_images, test_labels in test_loader: - out = model(test_images.to(device)) - test_labels = test_labels.to(device) - loss += loss_function(out, test_labels).item() - pred = out.argmax(dim=1) - for i in range(len(pred)): - y_true.append(test_labels[i].item()) - y_pred.append(pred[i].item()) - accuracy = sum([1 if t == p else 0 for t, p in zip(y_true, y_pred)]) / len( - test_loader.dataset - ) - return loss, accuracy diff --git a/examples/quickstart-monai/monaiexample/__init__.py b/examples/quickstart-monai/monaiexample/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/examples/quickstart-monai/monaiexample/client_app.py b/examples/quickstart-monai/monaiexample/client_app.py new file mode 100644 index 000000000000..c0dcac0cdae2 --- /dev/null +++ b/examples/quickstart-monai/monaiexample/client_app.py @@ -0,0 +1,41 @@ +"""monaiexample: A Flower / MONAI app.""" + +import torch +from flwr.common import Context +from flwr.client import NumPyClient, ClientApp + +from monaiexample.task import load_data, load_model, test, train, get_params, set_params + + +# Define Flower client +class FlowerClient(NumPyClient): + def __init__(self, net, trainloader, valloader): + self.net = net + self.trainloader = trainloader + self.valloader = valloader + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + def fit(self, parameters, config): + set_params(self.net, parameters) + train(self.net, self.trainloader, epoch_num=1, device=self.device) + return get_params(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config): + set_params(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader), {"accuracy": accuracy} + + +def client_fn(context: Context): + + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + + batch_size = context.run_config["batch-size"] + trainloader, valloader = load_data(num_partitions, partition_id, batch_size) + net = load_model() + + return FlowerClient(net, trainloader, valloader).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/quickstart-monai/monaiexample/server_app.py b/examples/quickstart-monai/monaiexample/server_app.py new file mode 100644 index 000000000000..f68d3887a488 --- /dev/null +++ b/examples/quickstart-monai/monaiexample/server_app.py @@ -0,0 +1,46 @@ +"""monaiexample: A Flower / MONAI app.""" + +from typing import List, Tuple + +from flwr.common import Metrics, Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg + +from monaiexample.task import load_model, get_params + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + + # Init model + model = load_model() + + # Convert model parameters to flwr.common.Parameters + ndarrays = get_params(model) + global_model_init = ndarrays_to_parameters(ndarrays) + + # Define strategy + fraction_fit = context.run_config["fraction-fit"] + strategy = FedAvg( + fraction_fit=fraction_fit, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=global_model_init, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/quickstart-monai/monaiexample/task.py b/examples/quickstart-monai/monaiexample/task.py new file mode 100644 index 000000000000..09597562a1f2 --- /dev/null +++ b/examples/quickstart-monai/monaiexample/task.py @@ -0,0 +1,199 @@ +"""monaiexample: A Flower / MONAI app.""" + +import os +import tarfile +from urllib import request +from collections import OrderedDict + +import torch +import monai +from monai.networks.nets import densenet +from monai.transforms import ( + Compose, + EnsureChannelFirst, + LoadImage, + RandFlip, + RandRotate, + RandZoom, + ScaleIntensity, + ToTensor, +) +from filelock import FileLock +from datasets import Dataset +from flwr_datasets.partitioner import IidPartitioner + + +def load_model(): + """Load a DenseNet12.""" + return densenet.DenseNet121(spatial_dims=2, in_channels=1, out_channels=6) + + +def get_params(model): + """Return tensors in the model's state_dict.""" + return [val.cpu().numpy() for _, val in model.state_dict().items()] + + +def set_params(model, ndarrays): + """Apply parameters to a model.""" + params_dict = zip(model.state_dict().keys(), ndarrays) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + model.load_state_dict(state_dict, strict=True) + + +def train(model, train_loader, epoch_num, device): + """Train a model using the supplied dataloader.""" + model.to(device) + loss_function = torch.nn.CrossEntropyLoss() + optimizer = torch.optim.Adam(model.parameters(), 1e-5) + for _ in range(epoch_num): + model.train() + for batch in train_loader: + images, labels = batch["img"], batch["label"] + optimizer.zero_grad() + loss_function(model(images.to(device)), labels.to(device)).backward() + optimizer.step() + + +def test(model, test_loader, device): + """Evaluate a model on a held-out dataset.""" + model.to(device) + model.eval() + loss = 0.0 + y_true = list() + y_pred = list() + loss_function = torch.nn.CrossEntropyLoss() + with torch.no_grad(): + for batch in test_loader: + images, labels = batch["img"], batch["label"] + out = model(images.to(device)) + labels = labels.to(device) + loss += loss_function(out, labels).item() + pred = out.argmax(dim=1) + for i in range(len(pred)): + y_true.append(labels[i].item()) + y_pred.append(pred[i].item()) + accuracy = sum([1 if t == p else 0 for t, p in zip(y_true, y_pred)]) / len( + test_loader.dataset + ) + return loss, accuracy + + +def _get_transforms(): + """Return transforms to be used for training and evaluation.""" + train_transforms = Compose( + [ + LoadImage(image_only=True), + EnsureChannelFirst(), + ScaleIntensity(), + RandRotate(range_x=15, prob=0.5, keep_size=True), + RandFlip(spatial_axis=0, prob=0.5), + RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5, keep_size=True), + ToTensor(), + ] + ) + + val_transforms = Compose( + [LoadImage(image_only=True), EnsureChannelFirst(), ScaleIntensity(), ToTensor()] + ) + + return train_transforms, val_transforms + + +def get_apply_transforms_fn(transforms_to_apply): + """Return a function that applies the transforms passed as input argument.""" + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [transforms_to_apply(img) for img in batch["img_file"]] + return batch + + return apply_transforms + + +ds = None +partitioner = None + + +def load_data(num_partitions, partition_id, batch_size): + """Download dataset, partition it and return data loader of specific partition.""" + # Set dataset and partitioner only once + global ds, partitioner + if ds is None: + image_file_list, image_label_list = _download_data() + + # Construct HuggingFace dataset + ds = Dataset.from_dict({"img_file": image_file_list, "label": image_label_list}) + # Set partitioner + partitioner = IidPartitioner(num_partitions) + partitioner.dataset = ds + + partition = partitioner.load_partition(partition_id) + + # Split train/validation + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + + # Get transforms + train_t, test_t = _get_transforms() + + # Apply transforms individually to each split + train_partition = partition_train_test["train"] + test_partition = partition_train_test["test"] + + partition_train = train_partition.with_transform(get_apply_transforms_fn(train_t)) + partition_val = test_partition.with_transform(get_apply_transforms_fn(test_t)) + + # Create dataloaders + train_loader = monai.data.DataLoader( + partition_train, batch_size=batch_size, shuffle=True + ) + val_loader = monai.data.DataLoader(partition_val, batch_size=batch_size) + + return train_loader, val_loader + + +def _download_data(): + """Download and extract dataset.""" + data_dir = "./MedNIST/" + _download_and_extract_if_needed( + "https://dl.dropboxusercontent.com/s/5wwskxctvcxiuea/MedNIST.tar.gz", + os.path.join(data_dir), + ) + + # Compute list of files and thier associated labels + class_names = sorted( + [x for x in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, x))] + ) + image_files = [ + [ + os.path.join(data_dir, class_name, x) + for x in os.listdir(os.path.join(data_dir, class_name)) + ] + for class_name in class_names + ] + image_file_list = [] + image_label_list = [] + for i, _ in enumerate(class_names): + image_file_list.extend(image_files[i]) + image_label_list.extend([i] * len(image_files[i])) + + return image_file_list, image_label_list + + +def _download_and_extract_if_needed(url, dest_folder): + """Download dataset if not present.""" + + # Logic behind a filelock to prevent multiple processes (e.g. ClientApps) + # from downloading the dataset at the same time. + with FileLock(".data_download.lock"): + if not os.path.isdir(dest_folder): + # Download the tar.gz file + tar_gz_filename = url.split("/")[-1] + if not os.path.isfile(tar_gz_filename): + with request.urlopen(url) as response, open( + tar_gz_filename, "wb" + ) as out_file: + out_file.write(response.read()) + + # Extract the tar.gz file + with tarfile.open(tar_gz_filename, "r:gz") as tar_ref: + tar_ref.extractall() diff --git a/examples/quickstart-monai/pyproject.toml b/examples/quickstart-monai/pyproject.toml index 2b77a2fc061f..6ecf5011d24f 100644 --- a/examples/quickstart-monai/pyproject.toml +++ b/examples/quickstart-monai/pyproject.toml @@ -1,19 +1,41 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "quickstart-monai" -version = "0.1.0" -description = "MONAI Federated Learning Quickstart with Flower" -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = ">=1.0,<2.0" -torch = "1.13.1" -tqdm = "4.66.3" -scikit-learn = "1.3.1" -monai = { version = "1.3.0", extras=["gdown", "nibabel", "tqdm", "itk"] } -numpy = "1.24.4" -pillow = "10.2.0" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "monaiexample" +version = "1.0.0" +description = "Federated Learning with MONAI and Flower (Quickstart Example)" +license = "Apache-2.0" +dependencies = [ + "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr-datasets[vision]>=0.3.0", + "monai==1.3.2", + "filelock==3.15.4", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "monaiexample.server_app:app" +clientapp = "monaiexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 5 +fraction-fit = 0.5 +batch-size = 128 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 + +[tool.flwr.federations.local-simulation-gpu] +options.num-supernodes = 10 +options.backend.client-resources.num-cpus = 4 +options.backend.client-resources.num-gpus = 0.25 # at most 4 ClientApps will run in a given GPU diff --git a/examples/quickstart-monai/requirements.txt b/examples/quickstart-monai/requirements.txt deleted file mode 100644 index e3f1e463c629..000000000000 --- a/examples/quickstart-monai/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -flwr>=1.0, <2.0 -torch==1.13.1 -tqdm==4.65.0 -scikit-learn==1.3.1 -monai[gdown,nibabel,tqdm,itk]==1.3.0 -numpy==1.24.4 -pillow==10.2.0 diff --git a/examples/quickstart-monai/run.sh b/examples/quickstart-monai/run.sh deleted file mode 100755 index 1da60bccb86d..000000000000 --- a/examples/quickstart-monai/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -python -c "from data import _download_data; _download_data()" - -echo "Starting server" -python server.py & -sleep 3 # Sleep for 3s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python client.py --partition-id $i & -done - -# Enable CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/quickstart-monai/server.py b/examples/quickstart-monai/server.py deleted file mode 100644 index fe691a88aba0..000000000000 --- a/examples/quickstart-monai/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List, Tuple - -import flwr as fl -from flwr.common import Metrics - - -# Define metric aggregation function -def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: - # Multiply accuracy of each client by number of examples used - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"accuracy": sum(accuracies) / sum(examples)} - - -# Define strategy -strategy = fl.server.strategy.FedAvg(evaluate_metrics_aggregation_fn=weighted_average) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) From 75ea504aedeca48634eb957cd163a6e27c888d64 Mon Sep 17 00:00:00 2001 From: Javier Date: Sat, 24 Aug 2024 17:31:33 +0100 Subject: [PATCH 129/188] refactor(examples) Update `vit-finetune` example (#3935) --- README.md | 2 +- doc/source/conf.py | 1 + .../README.md | 87 ++++++------ .../_static/central_evaluation.png | Bin examples/flowertune-vit/pyproject.toml | 43 ++++++ .../flowertune-vit/vitexample/__init__.py | 1 + .../flowertune-vit/vitexample/client_app.py | 62 +++++++++ .../flowertune-vit/vitexample/server_app.py | 77 ++++++++++ examples/flowertune-vit/vitexample/task.py | 131 ++++++++++++++++++ examples/vit-finetune/client.py | 80 ----------- examples/vit-finetune/dataset.py | 51 ------- examples/vit-finetune/main.py | 57 -------- examples/vit-finetune/model.py | 71 ---------- examples/vit-finetune/pyproject.toml | 17 --- examples/vit-finetune/requirements.txt | 5 - examples/vit-finetune/server.py | 61 -------- 16 files changed, 362 insertions(+), 384 deletions(-) rename examples/{vit-finetune => flowertune-vit}/README.md (56%) rename examples/{vit-finetune => flowertune-vit}/_static/central_evaluation.png (100%) create mode 100644 examples/flowertune-vit/pyproject.toml create mode 100644 examples/flowertune-vit/vitexample/__init__.py create mode 100644 examples/flowertune-vit/vitexample/client_app.py create mode 100644 examples/flowertune-vit/vitexample/server_app.py create mode 100644 examples/flowertune-vit/vitexample/task.py delete mode 100644 examples/vit-finetune/client.py delete mode 100644 examples/vit-finetune/dataset.py delete mode 100644 examples/vit-finetune/main.py delete mode 100644 examples/vit-finetune/model.py delete mode 100644 examples/vit-finetune/pyproject.toml delete mode 100644 examples/vit-finetune/requirements.txt delete mode 100644 examples/vit-finetune/server.py diff --git a/README.md b/README.md index 1dd686e5f1b6..3f1d96ca53c0 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Other [examples](https://github.com/adap/flower/tree/main/examples): - [Vertical FL](https://github.com/adap/flower/tree/main/examples/vertical-fl) - [Federated Finetuning of OpenAI's Whisper](https://github.com/adap/flower/tree/main/examples/whisper-federated-finetuning) - [Federated Finetuning of Large Language Model](https://github.com/adap/flower/tree/main/examples/llm-flowertune) -- [Federated Finetuning of a Vision Transformer](https://github.com/adap/flower/tree/main/examples/vit-finetune) +- [Federated Finetuning of a Vision Transformer](https://github.com/adap/flower/tree/main/examples/flowertune-vit) - [Advanced Flower with TensorFlow/Keras](https://github.com/adap/flower/tree/main/examples/advanced-tensorflow) - [Advanced Flower with PyTorch](https://github.com/adap/flower/tree/main/examples/advanced-pytorch) - Single-Machine Simulation of Federated Learning Systems ([PyTorch](https://github.com/adap/flower/tree/main/examples/simulation-pytorch)) ([Tensorflow](https://github.com/adap/flower/tree/main/examples/simulation-tensorflow)) diff --git a/doc/source/conf.py b/doc/source/conf.py index d3881325a5ce..5d434dd729bb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -195,6 +195,7 @@ def find_test_modules(package_path): "apiref-binaries": "ref-api-cli.html", "fedbn-example-pytorch-from-centralized-to-federated": "example-fedbn-pytorch-from-centralized-to-federated.html", "how-to-use-built-in-middleware-layers": "how-to-use-built-in-mods.html", + "vit-finetune": "flowertune-vit.html", # Restructuring: tutorials "tutorial/Flower-0-What-is-FL": "tutorial-series-what-is-federated-learning.html", "tutorial/Flower-1-Intro-to-FL-PyTorch": "tutorial-series-get-started-with-flower-pytorch.html", diff --git a/examples/vit-finetune/README.md b/examples/flowertune-vit/README.md similarity index 56% rename from examples/vit-finetune/README.md rename to examples/flowertune-vit/README.md index 957c0eda0b68..9e2b0fd6b079 100644 --- a/examples/vit-finetune/README.md +++ b/examples/flowertune-vit/README.md @@ -1,68 +1,78 @@ --- -title: Federated finetuning of a ViT -tags: [finetuneing, vision, fds] +tags: [finetuning, vision, fds] dataset: [Oxford Flower-102] framework: [torch, torchvision] --- -# Federated finetuning of a ViT +# Federated Finetuning of a Vision Transformer with Flower -This example shows how to use Flower's Simulation Engine to federate the finetuning of a Vision Transformer ([ViT-Base-16](https://pytorch.org/vision/main/models/generated/torchvision.models.vit_b_16.html#torchvision.models.vit_b_16)) that has been pretrained on ImageNet. To keep things simple we'll be finetuning it to [Oxford Flower-102](https://www.robots.ox.ac.uk/~vgg/data/flowers/102/index.html) datasset, creating 20 partitions using [Flower Datasets](https://flower.ai/docs/datasets/). We'll be finetuning just the exit `head` of the ViT, this means that the training is not that costly and each client requires just ~1GB of VRAM (for a batch size of 32 images). +This example shows how to use Flower's Simulation Engine to federate the finetuning of a Vision Transformer ([ViT-Base-16](https://pytorch.org/vision/main/models/generated/torchvision.models.vit_b_16.html#torchvision.models.vit_b_16)) that has been pretrained on ImageNet. To keep things simple we'll be finetuning it to [Oxford Flower-102](https://www.robots.ox.ac.uk/~vgg/data/flowers/102/index.html) datasset, creating 20 partitions using [Flower Datasets](https://flower.ai/docs/datasets/). We'll be finetuning just the exit `head` of the ViT, this means that the training is not that costly and each client requires just ~1GB of VRAM (for a batch size of 32 images) if you choose to use a GPU. -## Running the example +## Set up the project -If you haven't cloned the Flower repository already you might want to clone code example and discard the rest. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project + +Start by cloning the example project: ```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/vit-finetune . && rm -rf flower && cd vit-finetune +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/flowertune-vit . \ + && rm -rf _tmp \ + && cd flowertune-vit ``` -This will create a new directory called `vit-finetune` containing the following files: +This will create a new directory called `flowertune-vit` with the following structure: +```shell +flowertune-vit +β”œβ”€β”€ vitexample +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` --- README.md <- Your're reading this right now --- main.py <- Main file that launches the simulation --- client.py <- Contains Flower client code and ClientApp --- server.py <- Contains Flower server code and ServerApp --- model.py <- Defines model and train/eval functions --- dataset.py <- Downloads, partitions and processes dataset --- pyproject.toml <- Example dependencies, installable using Poetry --- requirements.txt <- Example dependencies, installable using pip -``` - -### Installing Dependencies -Project dependencies (such as `torch` and `flwr`) are defined in `pyproject.toml` and `requirements.txt`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)) or [pip](https://pip.pypa.io/en/latest/development/), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. +### Install dependencies and project -#### Poetry +Install the dependencies defined in `pyproject.toml` as well as the `vitexample` package. -```shell -poetry install -poetry shell +```bash +pip install -e . ``` -#### pip +## Run the project -With an activated environemnt, install the dependencies for this example: +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -```shell -pip install -r requirements.txt +### Run with the Simulation Engine + +> \[!TIP\] +> This example runs faster when the `ClientApp`s have access to a GPU. If your system has one, you can make use of it by configuring the `backend.client-resources` component in `pyproject.toml`. If you want to try running the example with GPU right away, use the `local-simulation-gpu` federation as shown below. + +```bash +# Run with the default federation (CPU only) +flwr run . ``` -### Run with `start_simulation()` +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: + +```bash +flwr run . --run-config num-server-rounds=5,batch-size=64 +``` -Running the example is quite straightforward. You can control the number of rounds `--num-rounds` (which defaults to 20). +Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 5x`ClientApp` will run in parallel in the available GPU. You can tweak the degree of parallelism by adjusting the settings of this federation in the `pyproject.toml`. ```bash -python main.py +# Run with the `local-simulation-gpu` federation +flwr run . local-simulation-gpu ``` ![](_static/central_evaluation.png) Running the example as-is on an RTX 3090Ti should take ~15s/round running 5 clients in parallel (plus the _global model_ during centralized evaluation stages) in a single GPU. Note that more clients could fit in VRAM, but since the GPU utilization is high (99%-100%) we are probably better off not doing that (at least in this case). -You can adjust the `client_resources` passed to `start_simulation()` so more/less clients run at the same time in the GPU. Take a look at the [Documentation](https://flower.ai/docs/framework/how-to-run-simulations.html) for more details on how you can customise your simulation. - ```bash +---------------------------------------------------------------------------------------+ | NVIDIA-SMI 535.161.07 Driver Version: 535.161.07 CUDA Version: 12.2 | @@ -90,12 +100,7 @@ You can adjust the `client_resources` passed to `start_simulation()` so more/les +---------------------------------------------------------------------------------------+ ``` -### Run with Flower Next (preview) +### Run with the Deployment Engine -```bash -flower-simulation \ - --client-app=client:app \ - --server-app=server:app \ - --num-supernodes=20 \ - --backend-config='{"client_resources": {"num_cpus":4, "num_gpus":0.25}}' -``` +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/vit-finetune/_static/central_evaluation.png b/examples/flowertune-vit/_static/central_evaluation.png similarity index 100% rename from examples/vit-finetune/_static/central_evaluation.png rename to examples/flowertune-vit/_static/central_evaluation.png diff --git a/examples/flowertune-vit/pyproject.toml b/examples/flowertune-vit/pyproject.toml new file mode 100644 index 000000000000..0f11dc54c81a --- /dev/null +++ b/examples/flowertune-vit/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "vitexample" +version = "1.0.0" +description = "Federated Finetuning of a Vision Transformer with Flower" +license = "Apache-2.0" +dependencies = [ + "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "vitexample.server_app:app" +clientapp = "vitexample.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +batch-size = 32 +learning-rate = 0.01 +dataset-name = "nelorth/oxford-flowers" +num-classes = 102 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 + +[tool.flwr.federations.local-simulation-gpu] +options.num-supernodes = 10 +options.backend.client-resources.num-cpus = 2 # each ClientApp assumes to use 2CPUs +options.backend.client-resources.num-gpus = 0.2 # at most 5 ClientApp will run in a given GPU diff --git a/examples/flowertune-vit/vitexample/__init__.py b/examples/flowertune-vit/vitexample/__init__.py new file mode 100644 index 000000000000..f0ce539fac90 --- /dev/null +++ b/examples/flowertune-vit/vitexample/__init__.py @@ -0,0 +1 @@ +"""vitexample: A Flower / PyTorch app with Vision Transformers.""" diff --git a/examples/flowertune-vit/vitexample/client_app.py b/examples/flowertune-vit/vitexample/client_app.py new file mode 100644 index 000000000000..59143f1d25f8 --- /dev/null +++ b/examples/flowertune-vit/vitexample/client_app.py @@ -0,0 +1,62 @@ +"""vitexample: A Flower / PyTorch app with Vision Transformers.""" + +import torch +from torch.utils.data import DataLoader + +from flwr.common import Context +from flwr.client import NumPyClient, ClientApp + + +from vitexample.task import apply_train_transforms, get_dataset_partition +from vitexample.task import get_model, set_params, get_params, train + + +class FedViTClient(NumPyClient): + def __init__(self, trainloader, learning_rate, num_classes): + self.trainloader = trainloader + self.learning_rate = learning_rate + self.model = get_model(num_classes) + + # Determine device + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.model.to(self.device) # send model to device + + def fit(self, parameters, config): + set_params(self.model, parameters) + + # Set optimizer + optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate) + # Train locally + avg_train_loss = train( + self.model, self.trainloader, optimizer, epochs=1, device=self.device + ) + # Return locally-finetuned part of the model + return ( + get_params(self.model), + len(self.trainloader.dataset), + {"train_loss": avg_train_loss}, + ) + + +def client_fn(context: Context): + """Return a FedViTClient.""" + + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + dataset_name = context.run_config["dataset-name"] + trainpartition = get_dataset_partition(num_partitions, partition_id, dataset_name) + + batch_size = context.run_config["batch-size"] + lr = context.run_config["learning-rate"] + num_classes = context.run_config["num-classes"] + trainset = trainpartition.with_transform(apply_train_transforms) + + trainloader = DataLoader( + trainset, batch_size=batch_size, num_workers=2, shuffle=True + ) + + return FedViTClient(trainloader, lr, num_classes).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/flowertune-vit/vitexample/server_app.py b/examples/flowertune-vit/vitexample/server_app.py new file mode 100644 index 000000000000..f37215df5eb9 --- /dev/null +++ b/examples/flowertune-vit/vitexample/server_app.py @@ -0,0 +1,77 @@ +"""vitexample: A Flower / PyTorch app with Vision Transformers.""" + +from logging import INFO + +import torch +from datasets import Dataset, load_dataset +from torch.utils.data import DataLoader + +from vitexample.task import apply_eval_transforms +from vitexample.task import get_model, set_params, test, get_params + +from flwr.common import Context, ndarrays_to_parameters +from flwr.common.logger import log +from flwr.server import ServerApp, ServerConfig, ServerAppComponents +from flwr.server.strategy import FedAvg + + +def get_evaluate_fn( + centralized_testset: Dataset, + num_classes: int, +): + """Return an evaluation function for centralized evaluation.""" + + def evaluate(server_round, parameters, config): + """Use the entire Oxford Flowers-102 test set for evaluation.""" + + # Determine device + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + # Instantiate model and apply current global parameters + model = get_model(num_classes) + set_params(model, parameters) + model.to(device) + + # Apply transform to dataset + testset = centralized_testset.with_transform(apply_eval_transforms) + + testloader = DataLoader(testset, batch_size=128) + # Run evaluation + loss, accuracy = test(model, testloader, device=device) + log(INFO, f"round: {server_round} -> acc: {accuracy:.4f}, loss: {loss: .4f}") + + return loss, {"accuracy": accuracy} + + return evaluate + + +def server_fn(context: Context): + + # Define tested for central evaluation + dataset_name = context.run_config["dataset-name"] + dataset = load_dataset(dataset_name) + test_set = dataset["test"] + + # Set initial global model + num_classes = context.run_config["num-classes"] + ndarrays = get_params(get_model(num_classes)) + init_parameters = ndarrays_to_parameters(ndarrays) + + # Configure the strategy + strategy = FedAvg( + fraction_fit=0.5, # Sample 50% of available clients + fraction_evaluate=0.0, # No federated evaluation + evaluate_fn=get_evaluate_fn( + test_set, num_classes + ), # Global evaluation function + initial_parameters=init_parameters, + ) + + # Construct ServerConfig + num_rounds = context.run_config["num-server-rounds"] + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +app = ServerApp(server_fn=server_fn) diff --git a/examples/flowertune-vit/vitexample/task.py b/examples/flowertune-vit/vitexample/task.py new file mode 100644 index 000000000000..3512d1891db2 --- /dev/null +++ b/examples/flowertune-vit/vitexample/task.py @@ -0,0 +1,131 @@ +"""vitexample: A Flower / PyTorch app with Vision Transformers.""" + +from collections import OrderedDict + +import torch +from torchvision.models import vit_b_16, ViT_B_16_Weights +from torchvision.transforms import ( + Compose, + Normalize, + ToTensor, + RandomResizedCrop, + Resize, + CenterCrop, +) + +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + + +def get_model(num_classes: int): + """Return a pretrained ViT with all layers frozen except output head.""" + + # Instantiate a pre-trained ViT-B on ImageNet + model = vit_b_16(weights=ViT_B_16_Weights.IMAGENET1K_V1) + + # We're going to federated the finetuning of this model + # using (by default) the Oxford Flowers-102 dataset. One easy way + # to achieve this is by re-initializing the output block of the + # ViT so it outputs 102 clases instead of the default 1k + in_features = model.heads[-1].in_features + model.heads[-1] = torch.nn.Linear(in_features, num_classes) + + # Disable gradients for everything + model.requires_grad_(False) + # Now enable just for output head + model.heads.requires_grad_(True) + + return model + + +def set_params(model, parameters): + """Apply the parameters to model head.""" + finetune_layers = model.heads + params_dict = zip(finetune_layers.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + finetune_layers.load_state_dict(state_dict, strict=True) + + +def get_params(model): + """Get parameters from model head as ndarrays.""" + finetune_layers = model.heads + return [val.cpu().numpy() for _, val in finetune_layers.state_dict().items()] + + +def train(net, trainloader, optimizer, epochs, device): + """Train the model on the training set.""" + criterion = torch.nn.CrossEntropyLoss() + net.train() + net.to(device) + avg_loss = 0 + # A very standard training loop for image classification + for _ in range(epochs): + for batch in trainloader: + images, labels = batch["image"].to(device), batch["label"].to(device) + optimizer.zero_grad() + loss = criterion(net(images), labels) + avg_loss += loss.item() / labels.shape[0] + loss.backward() + optimizer.step() + + return avg_loss / len(trainloader) + + +def test(net, testloader, device: str): + """Validate the network on the entire test set.""" + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + net.to(device) + net.eval() + with torch.no_grad(): + for data in testloader: + images, labels = data["image"].to(device), data["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + _, predicted = torch.max(outputs.data, 1) + correct += (predicted == labels).sum().item() + accuracy = correct / len(testloader.dataset) + return loss, accuracy + + +fds = None + + +def get_dataset_partition(num_partitions: int, partition_id: int, dataset_name: str): + """Get Oxford Flowers datasets and partition it.""" + global fds + if fds is None: + # Get dataset (by default Oxford Flowers-102) and create IID partitions + partitioner = IidPartitioner(num_partitions) + fds = FederatedDataset( + dataset=dataset_name, partitioners={"train": partitioner} + ) + + return fds.load_partition(partition_id) + + +def apply_eval_transforms(batch): + """Apply a very standard set of image transforms.""" + transforms = Compose( + [ + Resize((256, 256)), + CenterCrop((224, 224)), + ToTensor(), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] + ) + batch["image"] = [transforms(img) for img in batch["image"]] + return batch + + +def apply_train_transforms(batch): + """Apply a very standard set of image transforms.""" + transforms = Compose( + [ + RandomResizedCrop((224, 224)), + ToTensor(), + Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ] + ) + batch["image"] = [transforms(img) for img in batch["image"]] + return batch diff --git a/examples/vit-finetune/client.py b/examples/vit-finetune/client.py deleted file mode 100644 index 6226b9363ca4..000000000000 --- a/examples/vit-finetune/client.py +++ /dev/null @@ -1,80 +0,0 @@ -import flwr -import torch -from flwr.client import NumPyClient -from torch.utils.data import DataLoader - -from dataset import apply_transforms, get_dataset_with_partitions -from model import get_model, set_parameters, train - - -class FedViTClient(NumPyClient): - def __init__(self, trainset): - self.trainset = trainset - self.model = get_model() - - # Determine device - self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - self.model.to(self.device) # send model to device - - def set_for_finetuning(self): - """Freeze all parameter except those in the final head. - - Only output MLP will be updated by the client and therefore, the only part of - the model that will be federated (hence, communicated back to the server for - aggregation.) - """ - - # Disable gradients for everything - self.model.requires_grad_(False) - # Now enable just for output head - self.model.heads.requires_grad_(True) - - def get_parameters(self, config): - """Get locally updated parameters.""" - finetune_layers = self.model.heads - return [val.cpu().numpy() for _, val in finetune_layers.state_dict().items()] - - def fit(self, parameters, config): - set_parameters(self.model, parameters) - - # Get some info from the config - # Get batchsize and LR set from server - batch_size = config["batch_size"] - lr = config["lr"] - - trainloader = DataLoader( - self.trainset, batch_size=batch_size, num_workers=2, shuffle=True - ) - - # Set optimizer - optimizer = torch.optim.Adam(self.model.parameters(), lr=lr) - # Train locally - avg_train_loss = train( - self.model, trainloader, optimizer, epochs=1, device=self.device - ) - # Return locally-finetuned part of the model - return ( - self.get_parameters(config={}), - len(trainloader.dataset), - {"train_loss": avg_train_loss}, - ) - - -# Downloads and partition dataset -federated_ox_flowers, _ = get_dataset_with_partitions(num_partitions=20) - - -def client_fn(cid: str): - """Return a FedViTClient that trains with the cid-th data partition.""" - - trainset_for_this_client = federated_ox_flowers.load_partition(int(cid), "train") - - trainset = trainset_for_this_client.with_transform(apply_transforms) - - return FedViTClient(trainset).to_client() - - -# To be used with Flower Next -app = flwr.client.ClientApp( - client_fn=client_fn, -) diff --git a/examples/vit-finetune/dataset.py b/examples/vit-finetune/dataset.py deleted file mode 100644 index e1e01da61dd4..000000000000 --- a/examples/vit-finetune/dataset.py +++ /dev/null @@ -1,51 +0,0 @@ -from flwr_datasets import FederatedDataset -from torchvision.transforms import ( - CenterCrop, - Compose, - Normalize, - RandomResizedCrop, - Resize, - ToTensor, -) - - -def get_dataset_with_partitions(num_partitions: int): - """Get Oxford Flowers datasets and partition it. - - Return partitioned dataset as well as the whole test set. - """ - - # Get Oxford Flowers-102 and divide it into 20 IID partitions - ox_flowers_fds = FederatedDataset( - dataset="nelorth/oxford-flowers", partitioners={"train": num_partitions} - ) - - centralized_testset = ox_flowers_fds.load_split("test") - return ox_flowers_fds, centralized_testset - - -def apply_eval_transforms(batch): - """Apply a very standard set of image transforms.""" - transforms = Compose( - [ - Resize((256, 256)), - CenterCrop((224, 224)), - ToTensor(), - Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ] - ) - batch["image"] = [transforms(img) for img in batch["image"]] - return batch - - -def apply_transforms(batch): - """Apply a very standard set of image transforms.""" - transforms = Compose( - [ - RandomResizedCrop((224, 224)), - ToTensor(), - Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ] - ) - batch["image"] = [transforms(img) for img in batch["image"]] - return batch diff --git a/examples/vit-finetune/main.py b/examples/vit-finetune/main.py deleted file mode 100644 index 33ad78a04d47..000000000000 --- a/examples/vit-finetune/main.py +++ /dev/null @@ -1,57 +0,0 @@ -import argparse - -import flwr as fl -import matplotlib.pyplot as plt - -from client import client_fn -from server import strategy - -parser = argparse.ArgumentParser( - description="Finetuning of a ViT with Flower Simulation." -) - -parser.add_argument( - "--num-rounds", - type=int, - default=20, - help="Number of rounds.", -) - - -def main(): - args = parser.parse_args() - - # To control the degree of parallelism - # With default settings in this example, - # each client should take just ~1GB of VRAM. - client_resources = { - "num_cpus": 4, - "num_gpus": 0.2, - } - - # Launch simulation - history = fl.simulation.start_simulation( - client_fn=client_fn, - num_clients=20, - client_resources=client_resources, - config=fl.server.ServerConfig(num_rounds=args.num_rounds), - strategy=strategy, - ) - - print(history) - - # Basic plotting - global_accuracy_centralised = history.metrics_centralized["accuracy"] - round = [int(data[0]) for data in global_accuracy_centralised] - acc = [100.0 * data[1] for data in global_accuracy_centralised] - plt.plot(round, acc) - plt.xticks(round) - plt.grid() - plt.ylabel("Accuracy (%)") - plt.xlabel("Round") - plt.title("Federated finetuning of ViT for Flowers-102") - plt.savefig("central_evaluation.png") - - -if __name__ == "__main__": - main() diff --git a/examples/vit-finetune/model.py b/examples/vit-finetune/model.py deleted file mode 100644 index a0b8294aa485..000000000000 --- a/examples/vit-finetune/model.py +++ /dev/null @@ -1,71 +0,0 @@ -from collections import OrderedDict - -import torch -from torchvision.models import ViT_B_16_Weights, vit_b_16 - - -def get_model(): - """Return a pretrained ViT with all layers frozen except output head.""" - - # Instantiate a pre-trained ViT-B on ImageNet - model = vit_b_16(weights=ViT_B_16_Weights.IMAGENET1K_V1) - - # We're going to federated the finetuning of this model - # using the Oxford Flowers-102 dataset. One easy way to achieve - # this is by re-initializing the output block of the ViT so it - # outputs 102 clases instead of the default 1k - in_features = model.heads[-1].in_features - model.heads[-1] = torch.nn.Linear(in_features, 102) - - # Disable gradients for everything - model.requires_grad_(False) - # Now enable just for output head - model.heads.requires_grad_(True) - - return model - - -def set_parameters(model, parameters): - """Apply the parameters to the model. - - Recall this example only federates the head of the ViT so that's the only part of - the model we need to load. - """ - finetune_layers = model.heads - params_dict = zip(finetune_layers.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - finetune_layers.load_state_dict(state_dict, strict=True) - - -def train(net, trainloader, optimizer, epochs, device): - """Train the model on the training set.""" - criterion = torch.nn.CrossEntropyLoss() - net.train() - avg_loss = 0 - # A very standard training loop for image classification - for _ in range(epochs): - for batch in trainloader: - images, labels = batch["image"].to(device), batch["label"].to(device) - optimizer.zero_grad() - loss = criterion(net(images), labels) - avg_loss += loss.item() / labels.shape[0] - loss.backward() - optimizer.step() - - return avg_loss / len(trainloader) - - -def test(net, testloader, device: str): - """Validate the network on the entire test set.""" - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - net.eval() - with torch.no_grad(): - for data in testloader: - images, labels = data["image"].to(device), data["label"].to(device) - outputs = net(images) - loss += criterion(outputs, labels).item() - _, predicted = torch.max(outputs.data, 1) - correct += (predicted == labels).sum().item() - accuracy = correct / len(testloader.dataset) - return loss, accuracy diff --git a/examples/vit-finetune/pyproject.toml b/examples/vit-finetune/pyproject.toml deleted file mode 100644 index d014d6b6fb2a..000000000000 --- a/examples/vit-finetune/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "vit-finetune" -version = "0.1.0" -description = "FL finetuning of a Vision Transformer with Flower." -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = ">=3.8,<3.11" -flwr = { extras = ["simulation"], version = ">=1.0,<2.0" } -flwr-datasets = { extras = ["vision"], version = ">=0.0.2,<1.0.0" } -torch = "2.2.1" -torchvision = "0.17.1" -matplotlib = "3.8.3" diff --git a/examples/vit-finetune/requirements.txt b/examples/vit-finetune/requirements.txt deleted file mode 100644 index 3692be0d6c2c..000000000000 --- a/examples/vit-finetune/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flwr[simulation]>=1.0, <2.0 -flwr-datasets[vision]>=0.0.2, <1.0.0 -matplotlib==3.8.3 -torch==2.2.1 -torchvision==0.17.1 \ No newline at end of file diff --git a/examples/vit-finetune/server.py b/examples/vit-finetune/server.py deleted file mode 100644 index 5352d34c4fe2..000000000000 --- a/examples/vit-finetune/server.py +++ /dev/null @@ -1,61 +0,0 @@ -import flwr as fl -import torch -from datasets import Dataset -from torch.utils.data import DataLoader - -from dataset import apply_eval_transforms, get_dataset_with_partitions -from model import get_model, set_parameters, test - - -def fit_config(server_round: int): - """Return a configuration with static batch size and (local) epochs.""" - config = { - "lr": 0.01, # Learning rate used by clients - "batch_size": 32, # Batch size to use by clients during fit() - } - return config - - -def get_evaluate_fn( - centralized_testset: Dataset, -): - """Return an evaluation function for centralized evaluation.""" - - def evaluate(server_round, parameters, config): - """Use the entire Oxford Flowers-102 test set for evaluation.""" - - # Determine device - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - model = get_model() - set_parameters(model, parameters) - model.to(device) - - # Apply transform to dataset - testset = centralized_testset.with_transform(apply_eval_transforms) - - testloader = DataLoader(testset, batch_size=128) - # Run evaluation - loss, accuracy = test(model, testloader, device=device) - - return loss, {"accuracy": accuracy} - - return evaluate - - -# Downloads and partition dataset -_, centralized_testset = get_dataset_with_partitions(num_partitions=20) - -# Configure the strategy -strategy = fl.server.strategy.FedAvg( - fraction_fit=0.5, # Sample 50% of available clients for training each round - fraction_evaluate=0.0, # No federated evaluation - on_fit_config_fn=fit_config, - evaluate_fn=get_evaluate_fn(centralized_testset), # Global evaluation function -) - -# To be used with Flower Next -app = fl.server.ServerApp( - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, -) From 489247a64adeed412e8f6ea966e656b049a2fe71 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Sat, 24 Aug 2024 18:45:38 +0200 Subject: [PATCH 130/188] refactor(framework) Rename client auth to node auth (#4074) --- src/py/flwr/server/app.py | 26 ++--- src/py/flwr/server/server_test.py | 18 ++-- .../fleet/grpc_rere/server_interceptor.py | 22 ++--- .../grpc_rere/server_interceptor_test.py | 94 +++++++++---------- .../server/superlink/state/in_memory_state.py | 30 +++--- .../server/superlink/state/sqlite_state.py | 20 ++-- src/py/flwr/server/superlink/state/state.py | 16 ++-- .../flwr/server/superlink/state/state_test.py | 20 ++-- 8 files changed, 122 insertions(+), 124 deletions(-) diff --git a/src/py/flwr/server/app.py b/src/py/flwr/server/app.py index ef632a0c014d..32b490903554 100644 --- a/src/py/flwr/server/app.py +++ b/src/py/flwr/server/app.py @@ -278,24 +278,24 @@ def run_superlink() -> None: fleet_thread.start() bckg_threads.append(fleet_thread) elif args.fleet_api_type == TRANSPORT_TYPE_GRPC_RERE: - maybe_keys = _try_setup_client_authentication(args, certificates) + maybe_keys = _try_setup_node_authentication(args, certificates) interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None if maybe_keys is not None: ( - client_public_keys, + node_public_keys, server_private_key, server_public_key, ) = maybe_keys state = state_factory.state() - state.store_client_public_keys(client_public_keys) + state.store_node_public_keys(node_public_keys) state.store_server_private_public_key( private_key_to_bytes(server_private_key), public_key_to_bytes(server_public_key), ) log( INFO, - "Client authentication enabled with %d known public keys", - len(client_public_keys), + "Node authentication enabled with %d known public keys", + len(node_public_keys), ) interceptors = [AuthenticateServerInterceptor(state)] @@ -344,7 +344,7 @@ def _format_address(address: str) -> Tuple[str, str, int]: return (f"[{host}]:{port}" if is_v6 else f"{host}:{port}", host, port) -def _try_setup_client_authentication( +def _try_setup_node_authentication( args: argparse.Namespace, certificates: Optional[Tuple[bytes, bytes, bytes]], ) -> Optional[Tuple[Set[bytes], ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]: @@ -373,16 +373,16 @@ def _try_setup_client_authentication( "`--ssl-keyfile`, and `β€”-ssl-ca-certfile` and try again." ) - client_keys_file_path = Path(args.auth_list_public_keys) - if not client_keys_file_path.exists(): + node_keys_file_path = Path(args.auth_list_public_keys) + if not node_keys_file_path.exists(): sys.exit( "The provided path to the known public keys CSV file does not exist: " - f"{client_keys_file_path}. " + f"{node_keys_file_path}. " "Please provide the CSV file path containing known public keys " "to '--auth-list-public-keys'." ) - client_public_keys: Set[bytes] = set() + node_public_keys: Set[bytes] = set() try: ssh_private_key = load_ssh_private_key( @@ -413,13 +413,13 @@ def _try_setup_client_authentication( "path points to a valid public key file and try again." ) - with open(client_keys_file_path, newline="", encoding="utf-8") as csvfile: + with open(node_keys_file_path, newline="", encoding="utf-8") as csvfile: reader = csv.reader(csvfile) for row in reader: for element in row: public_key = load_ssh_public_key(element.encode()) if isinstance(public_key, ec.EllipticCurvePublicKey): - client_public_keys.add(public_key_to_bytes(public_key)) + node_public_keys.add(public_key_to_bytes(public_key)) else: sys.exit( "Error: Unable to parse the public keys in the CSV " @@ -427,7 +427,7 @@ def _try_setup_client_authentication( "known SSH public keys files and try again." ) return ( - client_public_keys, + node_public_keys, ssh_private_key, ssh_public_key, ) diff --git a/src/py/flwr/server/server_test.py b/src/py/flwr/server/server_test.py index f47b5c3d8469..b80811a6f730 100644 --- a/src/py/flwr/server/server_test.py +++ b/src/py/flwr/server/server_test.py @@ -55,7 +55,7 @@ ) from flwr.server.client_manager import SimpleClientManager -from .app import _try_setup_client_authentication +from .app import _try_setup_node_authentication from .client_proxy import ClientProxy from .server import Server, evaluate_clients, fit_clients @@ -203,8 +203,8 @@ def test_set_max_workers() -> None: assert server.max_workers == 42 -def test_setup_client_auth() -> None: # pylint: disable=R0914 - """Test setup client authentication.""" +def test_setup_node_auth() -> None: # pylint: disable=R0914 + """Test setup node authentication.""" # Prepare _, first_public_key = generate_key_pairs() private_key, public_key = generate_key_pairs() @@ -220,12 +220,12 @@ def test_setup_client_auth() -> None: # pylint: disable=R0914 # Execute with tempfile.TemporaryDirectory() as temp_dir: # Initialize temporary files - client_keys_file_path = Path(temp_dir) / "client_keys.csv" + node_keys_file_path = Path(temp_dir) / "node_keys.csv" server_private_key_path = Path(temp_dir) / "server_private_key" server_public_key_path = Path(temp_dir) / "server_public_key" # Fill the files with relevant keys - with open(client_keys_file_path, "w", newline="", encoding="utf-8") as csvfile: + with open(node_keys_file_path, "w", newline="", encoding="utf-8") as csvfile: writer = csv.writer(csvfile) writer.writerow( [ @@ -240,15 +240,15 @@ def test_setup_client_auth() -> None: # pylint: disable=R0914 server_public_key_path.write_bytes(server_public_key) server_private_key_path.write_bytes(server_private_key) - # Mock argparse with `require-client-authentication`` flag + # Mock argparse with `require-node-authentication`` flag mock_args = argparse.Namespace( - auth_list_public_keys=str(client_keys_file_path), + auth_list_public_keys=str(node_keys_file_path), auth_superlink_private_key=str(server_private_key_path), auth_superlink_public_key=str(server_public_key_path), ) - # Run _try_setup_client_authentication - result = _try_setup_client_authentication(mock_args, (b"", b"", b"")) + # Run _try_setup_node_authentication + result = _try_setup_node_authentication(mock_args, (b"", b"", b"")) expected_private_key = load_ssh_private_key(server_private_key, None) expected_public_key = load_ssh_public_key(server_public_key) diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py index 87ac45a4f9c8..70b38f8b625e 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py @@ -78,13 +78,13 @@ def _get_value_from_tuples( class AuthenticateServerInterceptor(grpc.ServerInterceptor): # type: ignore - """Server interceptor for client authentication.""" + """Server interceptor for node authentication.""" def __init__(self, state: State): self.state = state - self.client_public_keys = state.get_client_public_keys() - if len(self.client_public_keys) == 0: + self.node_public_keys = state.get_node_public_keys() + if len(self.node_public_keys) == 0: log(WARNING, "Authentication enabled, but no known public keys configured") private_key = self.state.get_server_private_key() @@ -103,9 +103,9 @@ def intercept_service( ) -> grpc.RpcMethodHandler: """Flower server interceptor authentication logic. - Intercept all unary calls from clients and authenticate clients by validating - auth metadata sent by the client. Continue RPC call if client is authenticated, - else, terminate RPC call by setting context to abort. + Intercept all unary calls from nodes and authenticate nodes by validating auth + metadata sent by the node. Continue RPC call if node is authenticated, else, + terminate RPC call by setting context to abort. """ # One of the method handlers in # `flwr.server.superlink.fleet.grpc_rere.fleet_server.FleetServicer` @@ -119,17 +119,17 @@ def _generic_method_handler( request: Request, context: grpc.ServicerContext, ) -> Response: - client_public_key_bytes = base64.urlsafe_b64decode( + node_public_key_bytes = base64.urlsafe_b64decode( _get_value_from_tuples( _PUBLIC_KEY_HEADER, context.invocation_metadata() ) ) - if client_public_key_bytes not in self.client_public_keys: + if node_public_key_bytes not in self.node_public_keys: context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied") if isinstance(request, CreateNodeRequest): response = self._create_authenticated_node( - client_public_key_bytes, request, context + node_public_key_bytes, request, context ) log( INFO, @@ -144,13 +144,13 @@ def _generic_method_handler( _AUTH_TOKEN_HEADER, context.invocation_metadata() ) ) - public_key = bytes_to_public_key(client_public_key_bytes) + public_key = bytes_to_public_key(node_public_key_bytes) if not self._verify_hmac(public_key, request, hmac_value): context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied") # Verify node_id - node_id = self.state.get_node_id(client_public_key_bytes) + node_id = self.state.get_node_id(node_public_key_bytes) if not self._verify_node_id(node_id, request): context.abort(grpc.StatusCode.UNAUTHENTICATED, "Access denied") diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py index ece443a816cb..74914be68a8f 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py @@ -58,7 +58,7 @@ class TestServerInterceptor(unittest.TestCase): # pylint: disable=R0902 def setUp(self) -> None: """Initialize mock stub and server interceptor.""" - self._client_private_key, self._client_public_key = generate_key_pairs() + self._node_private_key, self._node_public_key = generate_key_pairs() self._server_private_key, self._server_public_key = generate_key_pairs() state_factory = StateFactory(":flwr-in-memory-state:") @@ -69,9 +69,7 @@ def setUp(self) -> None: private_key_to_bytes(self._server_private_key), public_key_to_bytes(self._server_public_key), ) - self.state.store_client_public_keys( - {public_key_to_bytes(self._client_public_key)} - ) + self.state.store_node_public_keys({public_key_to_bytes(self._node_public_key)}) self._server_interceptor = AuthenticateServerInterceptor(self.state) self._server: grpc.Server = _run_fleet_api_grpc_rere( @@ -122,7 +120,7 @@ def test_successful_create_node_with_metadata(self) -> None: """Test server interceptor for creating node.""" # Prepare public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -145,9 +143,9 @@ def test_successful_create_node_with_metadata(self) -> None: def test_unsuccessful_create_node_with_metadata(self) -> None: """Test server interceptor for creating node unsuccessfully.""" # Prepare - _, client_public_key = generate_key_pairs() + _, node_public_key = generate_key_pairs() public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(client_public_key) + public_key_to_bytes(node_public_key) ) # Execute & Assert @@ -161,17 +159,17 @@ def test_successful_delete_node_with_metadata(self) -> None: """Test server interceptor for deleting node.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = DeleteNodeRequest(node=Node(node_id=node_id)) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -191,16 +189,16 @@ def test_unsuccessful_delete_node_with_metadata(self) -> None: """Test server interceptor for deleting node unsuccessfully.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = DeleteNodeRequest(node=Node(node_id=node_id)) - client_private_key, _ = generate_key_pairs() - shared_secret = generate_shared_key(client_private_key, self._server_public_key) + node_private_key, _ = generate_key_pairs() + shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute & Assert @@ -217,17 +215,17 @@ def test_successful_pull_task_ins_with_metadata(self) -> None: """Test server interceptor for pull task ins.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PullTaskInsRequest(node=Node(node_id=node_id)) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -247,16 +245,16 @@ def test_unsuccessful_pull_task_ins_with_metadata(self) -> None: """Test server interceptor for pull task ins unsuccessfully.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PullTaskInsRequest(node=Node(node_id=node_id)) - client_private_key, _ = generate_key_pairs() - shared_secret = generate_shared_key(client_private_key, self._server_public_key) + node_private_key, _ = generate_key_pairs() + shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute & Assert @@ -273,19 +271,19 @@ def test_successful_push_task_res_with_metadata(self) -> None: """Test server interceptor for push task res.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PushTaskResRequest( task_res_list=[TaskRes(task=Task(producer=Node(node_id=node_id)))] ) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -305,18 +303,18 @@ def test_unsuccessful_push_task_res_with_metadata(self) -> None: """Test server interceptor for push task res unsuccessfully.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PushTaskResRequest( task_res_list=[TaskRes(task=Task(producer=Node(node_id=node_id)))] ) - client_private_key, _ = generate_key_pairs() - shared_secret = generate_shared_key(client_private_key, self._server_public_key) + node_private_key, _ = generate_key_pairs() + shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute & Assert @@ -333,18 +331,18 @@ def test_successful_get_run_with_metadata(self) -> None: """Test server interceptor for pull task ins.""" # Prepare self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) run_id = self.state.create_run("", "", "", {}) request = GetRunRequest(run_id=run_id) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -364,17 +362,17 @@ def test_unsuccessful_get_run_with_metadata(self) -> None: """Test server interceptor for pull task ins unsuccessfully.""" # Prepare self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) run_id = self.state.create_run("", "", "", {}) request = GetRunRequest(run_id=run_id) - client_private_key, _ = generate_key_pairs() - shared_secret = generate_shared_key(client_private_key, self._server_public_key) + node_private_key, _ = generate_key_pairs() + shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute & Assert @@ -391,17 +389,17 @@ def test_successful_ping_with_metadata(self) -> None: """Test server interceptor for pull task ins.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PingRequest(node=Node(node_id=node_id)) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute @@ -421,16 +419,16 @@ def test_unsuccessful_ping_with_metadata(self) -> None: """Test server interceptor for pull task ins unsuccessfully.""" # Prepare node_id = self.state.create_node( - ping_interval=30, public_key=public_key_to_bytes(self._client_public_key) + ping_interval=30, public_key=public_key_to_bytes(self._node_public_key) ) request = PingRequest(node=Node(node_id=node_id)) - client_private_key, _ = generate_key_pairs() - shared_secret = generate_shared_key(client_private_key, self._server_public_key) + node_private_key, _ = generate_key_pairs() + shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) # Execute & Assert @@ -446,7 +444,7 @@ def test_unsuccessful_ping_with_metadata(self) -> None: def test_successful_restore_node(self) -> None: """Test server interceptor for restoring node.""" public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) response, call = self._create_node.with_call( request=CreateNodeRequest(), @@ -461,20 +459,20 @@ def test_successful_restore_node(self) -> None: ) node = response.node - client_node_id = node.node_id + node_node_id = node.node_id assert call.initial_metadata()[0] == expected_metadata assert isinstance(response, CreateNodeResponse) request = DeleteNodeRequest(node=node) shared_secret = generate_shared_key( - self._client_private_key, self._server_public_key + self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( compute_hmac(shared_secret, request.SerializeToString(True)) ) public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) response, call = self._delete_node.with_call( request=request, @@ -488,7 +486,7 @@ def test_successful_restore_node(self) -> None: assert grpc.StatusCode.OK == call.code() public_key_bytes = base64.urlsafe_b64encode( - public_key_to_bytes(self._client_public_key) + public_key_to_bytes(self._node_public_key) ) response, call = self._create_node.with_call( request=CreateNodeRequest(), @@ -504,4 +502,4 @@ def test_successful_restore_node(self) -> None: assert call.initial_metadata()[0] == expected_metadata assert isinstance(response, CreateNodeResponse) - assert response.node.node_id == client_node_id + assert response.node.node_id == node_node_id diff --git a/src/py/flwr/server/superlink/state/in_memory_state.py b/src/py/flwr/server/superlink/state/in_memory_state.py index fde8fe41912f..c87ba86e47e7 100644 --- a/src/py/flwr/server/superlink/state/in_memory_state.py +++ b/src/py/flwr/server/superlink/state/in_memory_state.py @@ -45,7 +45,7 @@ def __init__(self) -> None: self.task_ins_store: Dict[UUID, TaskIns] = {} self.task_res_store: Dict[UUID, TaskRes] = {} - self.client_public_keys: Set[bytes] = set() + self.node_public_keys: Set[bytes] = set() self.server_public_key: Optional[bytes] = None self.server_private_key: Optional[bytes] = None @@ -237,7 +237,7 @@ def create_node( return node_id def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None: - """Delete a client node.""" + """Delete a node.""" with self.lock: if node_id not in self.node_ids: raise ValueError(f"Node {node_id} not found") @@ -254,7 +254,7 @@ def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None: del self.node_ids[node_id] def get_nodes(self, run_id: int) -> Set[int]: - """Return all available client nodes. + """Return all available nodes. Constraints ----------- @@ -271,9 +271,9 @@ def get_nodes(self, run_id: int) -> Set[int]: if online_until > current_time } - def get_node_id(self, client_public_key: bytes) -> Optional[int]: - """Retrieve stored `node_id` filtered by `client_public_keys`.""" - return self.public_key_to_node_id.get(client_public_key) + def get_node_id(self, node_public_key: bytes) -> Optional[int]: + """Retrieve stored `node_id` filtered by `node_public_keys`.""" + return self.public_key_to_node_id.get(node_public_key) def create_run( self, @@ -318,19 +318,19 @@ def get_server_public_key(self) -> Optional[bytes]: """Retrieve `server_public_key` in urlsafe bytes.""" return self.server_public_key - def store_client_public_keys(self, public_keys: Set[bytes]) -> None: - """Store a set of `client_public_keys` in state.""" + def store_node_public_keys(self, public_keys: Set[bytes]) -> None: + """Store a set of `node_public_keys` in state.""" with self.lock: - self.client_public_keys = public_keys + self.node_public_keys = public_keys - def store_client_public_key(self, public_key: bytes) -> None: - """Store a `client_public_key` in state.""" + def store_node_public_key(self, public_key: bytes) -> None: + """Store a `node_public_key` in state.""" with self.lock: - self.client_public_keys.add(public_key) + self.node_public_keys.add(public_key) - def get_client_public_keys(self) -> Set[bytes]: - """Retrieve all currently stored `client_public_keys` as a set.""" - return self.client_public_keys + def get_node_public_keys(self) -> Set[bytes]: + """Retrieve all currently stored `node_public_keys` as a set.""" + return self.node_public_keys def get_run(self, run_id: int) -> Optional[Run]: """Retrieve information about the run with the specified `run_id`.""" diff --git a/src/py/flwr/server/superlink/state/sqlite_state.py b/src/py/flwr/server/superlink/state/sqlite_state.py index 93b3cd63ca7f..daa211560912 100644 --- a/src/py/flwr/server/superlink/state/sqlite_state.py +++ b/src/py/flwr/server/superlink/state/sqlite_state.py @@ -569,7 +569,7 @@ def create_node( return node_id def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None: - """Delete a client node.""" + """Delete a node.""" query = "DELETE FROM node WHERE node_id = ?" params = (node_id,) @@ -607,10 +607,10 @@ def get_nodes(self, run_id: int) -> Set[int]: result: Set[int] = {row["node_id"] for row in rows} return result - def get_node_id(self, client_public_key: bytes) -> Optional[int]: - """Retrieve stored `node_id` filtered by `client_public_keys`.""" + def get_node_id(self, node_public_key: bytes) -> Optional[int]: + """Retrieve stored `node_id` filtered by `node_public_keys`.""" query = "SELECT node_id FROM node WHERE public_key = :public_key;" - row = self.query(query, {"public_key": client_public_key}) + row = self.query(query, {"public_key": node_public_key}) if len(row) > 0: node_id: int = row[0]["node_id"] return node_id @@ -684,19 +684,19 @@ def get_server_public_key(self) -> Optional[bytes]: public_key = None return public_key - def store_client_public_keys(self, public_keys: Set[bytes]) -> None: - """Store a set of `client_public_keys` in state.""" + def store_node_public_keys(self, public_keys: Set[bytes]) -> None: + """Store a set of `node_public_keys` in state.""" query = "INSERT INTO public_key (public_key) VALUES (?)" data = [(key,) for key in public_keys] self.query(query, data) - def store_client_public_key(self, public_key: bytes) -> None: - """Store a `client_public_key` in state.""" + def store_node_public_key(self, public_key: bytes) -> None: + """Store a `node_public_key` in state.""" query = "INSERT INTO public_key (public_key) VALUES (:public_key)" self.query(query, {"public_key": public_key}) - def get_client_public_keys(self) -> Set[bytes]: - """Retrieve all currently stored `client_public_keys` as a set.""" + def get_node_public_keys(self) -> Set[bytes]: + """Retrieve all currently stored `node_public_keys` as a set.""" query = "SELECT public_key FROM public_key" rows = self.query(query) result: Set[bytes] = {row["public_key"] for row in rows} diff --git a/src/py/flwr/server/superlink/state/state.py b/src/py/flwr/server/superlink/state/state.py index 80d3b799bce3..fea53105b23f 100644 --- a/src/py/flwr/server/superlink/state/state.py +++ b/src/py/flwr/server/superlink/state/state.py @@ -153,8 +153,8 @@ def get_nodes(self, run_id: int) -> Set[int]: """ @abc.abstractmethod - def get_node_id(self, client_public_key: bytes) -> Optional[int]: - """Retrieve stored `node_id` filtered by `client_public_keys`.""" + def get_node_id(self, node_public_key: bytes) -> Optional[int]: + """Retrieve stored `node_id` filtered by `node_public_keys`.""" @abc.abstractmethod def create_run( @@ -199,16 +199,16 @@ def get_server_public_key(self) -> Optional[bytes]: """Retrieve `server_public_key` in urlsafe bytes.""" @abc.abstractmethod - def store_client_public_keys(self, public_keys: Set[bytes]) -> None: - """Store a set of `client_public_keys` in state.""" + def store_node_public_keys(self, public_keys: Set[bytes]) -> None: + """Store a set of `node_public_keys` in state.""" @abc.abstractmethod - def store_client_public_key(self, public_key: bytes) -> None: - """Store a `client_public_key` in state.""" + def store_node_public_key(self, public_key: bytes) -> None: + """Store a `node_public_key` in state.""" @abc.abstractmethod - def get_client_public_keys(self) -> Set[bytes]: - """Retrieve all currently stored `client_public_keys` as a set.""" + def get_node_public_keys(self) -> Set[bytes]: + """Retrieve all currently stored `node_public_keys` as a set.""" @abc.abstractmethod def acknowledge_ping(self, node_id: int, ping_interval: float) -> bool: diff --git a/src/py/flwr/server/superlink/state/state_test.py b/src/py/flwr/server/superlink/state/state_test.py index 3efce9ca0c88..0cf30a42ca2c 100644 --- a/src/py/flwr/server/superlink/state/state_test.py +++ b/src/py/flwr/server/superlink/state/state_test.py @@ -575,22 +575,22 @@ def test_store_server_private_public_key_twice(self) -> None: new_private_key_bytes, new_public_key_bytes ) - def test_client_public_keys(self) -> None: - """Test store_client_public_keys and get_client_public_keys from state.""" + def test_node_public_keys(self) -> None: + """Test store_node_public_keys and get_node_public_keys from state.""" # Prepare state: State = self.state_factory() key_pairs = [generate_key_pairs() for _ in range(3)] public_keys = {public_key_to_bytes(pair[1]) for pair in key_pairs} # Execute - state.store_client_public_keys(public_keys) - client_public_keys = state.get_client_public_keys() + state.store_node_public_keys(public_keys) + node_public_keys = state.get_node_public_keys() # Assert - assert client_public_keys == public_keys + assert node_public_keys == public_keys - def test_client_public_key(self) -> None: - """Test store_client_public_key and get_client_public_keys from state.""" + def test_node_public_key(self) -> None: + """Test store_node_public_key and get_node_public_keys from state.""" # Prepare state: State = self.state_factory() key_pairs = [generate_key_pairs() for _ in range(3)] @@ -598,11 +598,11 @@ def test_client_public_key(self) -> None: # Execute for public_key in public_keys: - state.store_client_public_key(public_key) - client_public_keys = state.get_client_public_keys() + state.store_node_public_key(public_key) + node_public_keys = state.get_node_public_keys() # Assert - assert client_public_keys == public_keys + assert node_public_keys == public_keys def test_acknowledge_ping(self) -> None: """Test if acknowledge_ping works and if get_nodes return online nodes.""" From 2612332fc26696d972344d8e08d3f4fdc5638944 Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 24 Aug 2024 19:32:08 +0200 Subject: [PATCH 131/188] feat(framework) Log `node_id`, `fab_hash` and `run_id` on SuperLink (#4079) Signed-off-by: Danny Heinrich Co-authored-by: Daniel J. Beutel --- .../fleet/grpc_rere/fleet_servicer.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py index e0501e54fafc..02e34e0bba02 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py @@ -51,19 +51,22 @@ def CreateNode( self, request: CreateNodeRequest, context: grpc.ServicerContext ) -> CreateNodeResponse: """.""" - log(INFO, "FleetServicer.CreateNode") + log(INFO, "[Fleet.CreateNode] Request ping_interval=%s", request.ping_interval) + log(DEBUG, "[Fleet.CreateNode] Request: %s", request) response = message_handler.create_node( request=request, state=self.state_factory.state(), ) - log(INFO, "FleetServicer: Created node_id=%s", response.node.node_id) + log(INFO, "[Fleet.CreateNode] Created node_id=%s", response.node.node_id) + log(DEBUG, "[Fleet.CreateNode] Response: %s", response) return response def DeleteNode( self, request: DeleteNodeRequest, context: grpc.ServicerContext ) -> DeleteNodeResponse: """.""" - log(INFO, "FleetServicer.DeleteNode") + log(INFO, "[Fleet.DeleteNode] Delete node_id=%s", request.node.node_id) + log(DEBUG, "[Fleet.DeleteNode] Request: %s", request) return message_handler.delete_node( request=request, state=self.state_factory.state(), @@ -71,7 +74,7 @@ def DeleteNode( def Ping(self, request: PingRequest, context: grpc.ServicerContext) -> PingResponse: """.""" - log(DEBUG, "FleetServicer.Ping") + log(DEBUG, "[Fleet.Ping] Request: %s", request) return message_handler.ping( request=request, state=self.state_factory.state(), @@ -81,7 +84,8 @@ def PullTaskIns( self, request: PullTaskInsRequest, context: grpc.ServicerContext ) -> PullTaskInsResponse: """Pull TaskIns.""" - log(INFO, "FleetServicer.PullTaskIns") + log(INFO, "[Fleet.PullTaskIns] node_id=%s", request.node.node_id) + log(DEBUG, "[Fleet.PullTaskIns] Request: %s", request) return message_handler.pull_task_ins( request=request, state=self.state_factory.state(), @@ -91,7 +95,14 @@ def PushTaskRes( self, request: PushTaskResRequest, context: grpc.ServicerContext ) -> PushTaskResResponse: """Push TaskRes.""" - log(INFO, "FleetServicer.PushTaskRes") + if request.task_res_list: + log( + INFO, + "[Fleet.PushTaskRes] Push results from node_id=%s", + request.task_res_list[0].task.producer.node_id, + ) + else: + log(INFO, "[Fleet.PushTaskRes] No task results to push") return message_handler.push_task_res( request=request, state=self.state_factory.state(), @@ -101,7 +112,7 @@ def GetRun( self, request: GetRunRequest, context: grpc.ServicerContext ) -> GetRunResponse: """Get run information.""" - log(INFO, "FleetServicer.GetRun") + log(INFO, "[Fleet.GetRun] Requesting `Run` for run_id=%s", request.run_id) return message_handler.get_run( request=request, state=self.state_factory.state(), @@ -111,7 +122,7 @@ def GetFab( self, request: GetFabRequest, context: grpc.ServicerContext ) -> GetFabResponse: """Get FAB.""" - log(DEBUG, "DriverServicer.GetFab") + log(INFO, "[Fleet.GetFab] Requesting FAB for fab_hash=%s", request.hash_str) return message_handler.get_fab( request=request, ffs=self.ffs_factory.ffs(), From 559408bd29baf3119b6a57a5c4afa197d9367bdd Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Mon, 26 Aug 2024 13:09:54 +0200 Subject: [PATCH 132/188] break(framework) Remove `flwr example` command (#4084) --- src/py/flwr/cli/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/py/flwr/cli/app.py b/src/py/flwr/cli/app.py index d1b270026cd7..93effea6df98 100644 --- a/src/py/flwr/cli/app.py +++ b/src/py/flwr/cli/app.py @@ -18,7 +18,6 @@ from typer.main import get_command from .build import build -from .example import example from .install import install from .new import new from .run import run @@ -33,7 +32,6 @@ ) app.command()(new) -app.command()(example) app.command()(run) app.command()(build) app.command()(install) From 6651729128c189408270c3dbe5256dd22deaa03a Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:05:32 +0200 Subject: [PATCH 133/188] feat(datasets) Add grouped natural id partitioner (#4051) Co-authored-by: Carlos Mari Co-authored-by: jafermarq --- .../flwr_datasets/partitioner/__init__.py | 2 + .../grouped_natural_id_partitioner.py | 224 +++++++++++++ .../grouped_natural_id_partitioner_test.py | 310 ++++++++++++++++++ 3 files changed, 536 insertions(+) create mode 100644 datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner.py create mode 100644 datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner_test.py diff --git a/datasets/flwr_datasets/partitioner/__init__.py b/datasets/flwr_datasets/partitioner/__init__.py index bb35d5a85cdc..acb2e6e832f5 100644 --- a/datasets/flwr_datasets/partitioner/__init__.py +++ b/datasets/flwr_datasets/partitioner/__init__.py @@ -18,6 +18,7 @@ from .dirichlet_partitioner import DirichletPartitioner from .distribution_partitioner import DistributionPartitioner from .exponential_partitioner import ExponentialPartitioner +from .grouped_natural_id_partitioner import GroupedNaturalIdPartitioner from .iid_partitioner import IidPartitioner from .inner_dirichlet_partitioner import InnerDirichletPartitioner from .linear_partitioner import LinearPartitioner @@ -32,6 +33,7 @@ "DirichletPartitioner", "DistributionPartitioner", "ExponentialPartitioner", + "GroupedNaturalIdPartitioner", "IidPartitioner", "InnerDirichletPartitioner", "LinearPartitioner", diff --git a/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner.py b/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner.py new file mode 100644 index 000000000000..f10d80b3aaac --- /dev/null +++ b/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner.py @@ -0,0 +1,224 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Grouped natural id partitioner class that works with Hugging Face Datasets.""" + + +from typing import Any, Dict, List, Literal + +import numpy as np + +import datasets +from flwr_datasets.common.typing import NDArrayInt +from flwr_datasets.partitioner.partitioner import Partitioner + + +class GroupedNaturalIdPartitioner(Partitioner): + """Partition dataset by creating groups of natural ids. + + Conceptually, you can think of this partitioner as a way of creating an organization + of x users instead of each user represetning a separate partition. You can change + the nature of the problem from cross-device to cross-silo (cross organization). + + Parameters + ---------- + partition_by: str + The name of the column that contains the unique values of partitions. + group_size: int + The number of unique ids that will be placed in a single group. + mode: Literal["allow-smaller", "allow-bigger", "drop-reminder", ""strict"] + The mode that will be used to handle the remainder of the unique ids. + - "allow-smaller": The last group can be smaller than the group_size. + - "allow-bigger": The first group can be bigger than the group_size. + - "drop-reminder": The last group will be dropped if it is smaller than the + group_size. + - "strict": Raises a ValueError if the remainder is not zero. In this mode, you + expect each group to have the same size. + sort_unique_ids: bool + If True, the unique natural ids will be sorted before creating the groups. + + Examples + -------- + Partition users in the "sentiment140" (aka Twitter) dataset into groups of two + users following the default mode: + + >>> from flwr_datasets import FederatedDataset + >>> from flwr_datasets.partitioner import GroupedNaturalIdPartitioner + >>> + >>> partitioner = GroupedNaturalIdPartitioner(partition_by="user", group_size=2) + >>> fds = FederatedDataset(dataset="sentiment140", + >>> partitioners={"train": partitioner}) + >>> partition = fds.load_partition(0) + """ + + def __init__( + self, + partition_by: str, + group_size: int, + mode: Literal[ + "allow-smaller", "allow-bigger", "drop-reminder", "strict" + ] = "allow-smaller", + sort_unique_ids: bool = False, + ) -> None: + super().__init__() + self._partition_id_to_natural_ids: Dict[int, List[Any]] = {} + self._natural_id_to_partition_id: Dict[Any, int] = {} + self._partition_id_to_indices: Dict[int, NDArrayInt] = {} + self._partition_by = partition_by + self._mode = mode + self._sort_unique_ids = sort_unique_ids + + if group_size < 0: + raise ValueError("group_size must be a positive integer") + self._group_size = group_size + + def _create_int_partition_id_to_natural_id(self) -> None: + """Create a mapping from int indices to unique client ids from dataset. + + Natural ids come from the column specified in `partition_by`. + """ + unique_natural_ids = self.dataset.unique(self._partition_by) + if self._mode != "allow-smaller" and self._group_size > len(unique_natural_ids): + raise ValueError( + "The group size needs to be smaller than the number of the unique " + "natural ids unless you are using allow-smaller mode which will " + "result in a single partition." + ) + if self._sort_unique_ids: + unique_natural_ids = sorted(unique_natural_ids) + num_unique_natural_ids = len(unique_natural_ids) + remainder = num_unique_natural_ids % self._group_size + num_groups = num_unique_natural_ids // self._group_size + if num_groups == 0 and self._mode == "allow-smaller": + num_groups = 1 + remainder = 0 + # Note that the number of groups might be different that this number + # due to certain modes, it's a base value. + + if self._mode == "allow-bigger": + groups_of_natural_ids = np.array_split(unique_natural_ids, num_groups) + elif self._mode == "drop-reminder": + # Narrow down the unique_natural_ids to not have a bigger group + # which is the behavior of the np.array_split + unique_natural_ids = unique_natural_ids[ + : int(num_groups * self._group_size) + ] + groups_of_natural_ids = np.array_split(unique_natural_ids, num_groups) + elif self._mode == "allow-smaller": + if remainder > 0: + last_group_ids = unique_natural_ids[-remainder:] + unique_natural_ids = unique_natural_ids[ + : int(num_groups * self._group_size) + ] + groups_of_natural_ids = np.array_split(unique_natural_ids, num_groups) + if remainder > 0: + groups_of_natural_ids.append(np.array(last_group_ids)) + elif self._mode == "strict": + if remainder != 0: + raise ValueError( + "Strict mode requires that the number of unique natural ids is " + "perfectly divisible by the group size. " + f"Found remainder: {remainder}. Please pass the group_size that " + f"enables strict mode or relax the mode parameter. Refer to the " + f"documentation of the mode parameter for the available modes." + ) + groups_of_natural_ids = np.array_split(unique_natural_ids, num_groups) + else: + raise ValueError( + f"Given {self._mode} is not a valid mode. Refer to the documentation of" + " the mode parameter for the available modes." + ) + + self._partition_id_to_natural_ids = {} + for group_of_natural_ids_id, group_of_natural_ids in enumerate( + groups_of_natural_ids + ): + self._partition_id_to_natural_ids[group_of_natural_ids_id] = ( + group_of_natural_ids.tolist() + ) + + def _create_natural_id_to_int_partition_id(self) -> None: + """Create a mapping from unique client ids from dataset to int indices. + + Natural ids come from the column specified in `partition_by`. This object is + inverse of the `self._partition_id_to_natural_id`. This method assumes that + `self._partition_id_to_natural_id` already exists. + """ + self._natural_id_to_partition_id = {} + for partition_id, natural_ids in self._partition_id_to_natural_ids.items(): + for natural_id in natural_ids: + self._natural_id_to_partition_id[natural_id] = partition_id + + def _create_partition_id_to_indices(self) -> None: + natural_id_to_indices = {} # type: ignore + natural_ids = np.array(self.dataset[self._partition_by]) + + for index, natural_id in enumerate(natural_ids): + if natural_id not in natural_id_to_indices: + natural_id_to_indices[natural_id] = [] + natural_id_to_indices[natural_id].append(index) + + self._partition_id_to_indices = {} + for partition_id, natural_id_group in self._partition_id_to_natural_ids.items(): + indices = [] + for natural_id in natural_id_group: + indices.extend(natural_id_to_indices[natural_id]) + self._partition_id_to_indices[partition_id] = np.array(indices) + + def load_partition(self, partition_id: int) -> datasets.Dataset: + """Load a single partition corresponding to a single `partition_id`. + + The choice of the partition is based on unique integers assigned to each + natural id present in the dataset in the `partition_by` column. + + + Parameters + ---------- + partition_id : int + the index that corresponds to the requested partition + + Returns + ------- + dataset_partition : Dataset + single dataset partition + """ + if len(self._partition_id_to_natural_ids) == 0: + self._create_int_partition_id_to_natural_id() + self._create_natural_id_to_int_partition_id() + + if len(self._partition_id_to_indices) == 0: + self._create_partition_id_to_indices() + + return self.dataset.select(self._partition_id_to_indices[partition_id]) + + @property + def num_partitions(self) -> int: + """Total number of partitions.""" + if len(self._partition_id_to_natural_ids) == 0: + self._create_int_partition_id_to_natural_id() + self._create_natural_id_to_int_partition_id() + return len(self._partition_id_to_natural_ids) + + @property + def partition_id_to_natural_ids(self) -> Dict[int, List[Any]]: + """Partition id to the corresponding group of natural ids present. + + Natural ids are the unique values in `partition_by` column in dataset. + """ + return self._partition_id_to_natural_ids + + @property + def natural_id_to_partition_id(self) -> Dict[Any, int]: + """Natural id to the corresponding partition id.""" + return self._natural_id_to_partition_id diff --git a/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner_test.py b/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner_test.py new file mode 100644 index 000000000000..635d3850624d --- /dev/null +++ b/datasets/flwr_datasets/partitioner/grouped_natural_id_partitioner_test.py @@ -0,0 +1,310 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Test GroupedNaturalIdPartitioner.""" + + +import unittest +from typing import List, Literal, Set + +from parameterized import parameterized, parameterized_class + +from datasets import Dataset +from flwr_datasets.partitioner.grouped_natural_id_partitioner import ( + GroupedNaturalIdPartitioner, +) + + +def _create_dataset(num_rows: int, n_unique_natural_ids: int) -> Dataset: + """Create dataset based on the number of rows and unique natural ids.""" + data = { + "features": list(range(num_rows)), + "natural_id": [f"{i % n_unique_natural_ids}" for i in range(num_rows)], + "labels": [i % 2 for i in range(num_rows)], + } + dataset = Dataset.from_dict(data) + return dataset + + +# mypy: disable-error-code="attr-defined" +@parameterized_class( + ("sort_unique_ids",), + [ + (False,), + (True,), + ], +) +# pylint: disable=no-member +class TestGroupedNaturalIdPartitioner(unittest.TestCase): + """Test GroupedNaturalIdPartitioner.""" + + @parameterized.expand( # type: ignore + # num_rows, num_unique_natural_ids, group_size, expected_num_partitions + [ + [10, 10, 2, 5], + [11, 10, 2, 5], + [100, 10, 2, 5], + [12, 6, 3, 2], + ] + ) + def test_strict_mode_num_partitions_and_partition_sizes( + self, + num_rows: int, + num_unique_natural_id: int, + group_size: int, + expected_num_partitions: int, + ) -> None: + """Test strict mode with valid group size.""" + dataset = _create_dataset(num_rows, num_unique_natural_id) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=group_size, + mode="strict", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + # Trigger partitioning + _ = partitioner.load_partition(0) + self.assertEqual(partitioner.num_partitions, expected_num_partitions) + + @parameterized.expand( # type: ignore + # num_rows, num_unique_natural_ids, group_size, expected_num_partitions, + # expected_num_unique_natural_ids + [ + [10, 10, 2, [2, 2, 2, 2, 2]], + [100, 10, 2, [2, 2, 2, 2, 2]], + [12, 6, 3, [3, 3]], + # The cases in which the partitions should be smaller + [10, 7, 2, [2, 2, 2, 1]], + [10, 3, 2, [2, 1]], + ] + ) + def test_allow_smaller_mode_num_partitions_and_partition_sizes( + self, + num_rows: int, + num_unique_natural_id: int, + group_size: int, + expected_num_unique_natural_ids: List[int], + ) -> None: + """Test allow-smaller mode handles the remainder correctly.""" + dataset = _create_dataset(num_rows, num_unique_natural_id) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=group_size, + mode="allow-smaller", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + # Trigger partitioning + partitions = [ + partitioner.load_partition(i) for i in range(partitioner.num_partitions) + ] + unique_natural_ids = [ + len(partition.unique("natural_id")) for partition in partitions + ] + self.assertEqual(unique_natural_ids, expected_num_unique_natural_ids) + + @parameterized.expand( # type: ignore + # num_rows, num_unique_natural_ids, group_size, expected_num_partitions, + # expected_num_unique_natural_ids + [ + [10, 10, 2, [2, 2, 2, 2, 2]], + [100, 10, 2, [2, 2, 2, 2, 2]], + [12, 6, 3, [3, 3]], + # The cases in which the partitions should be smaller + [10, 7, 2, [3, 2, 2]], + [10, 3, 2, [3]], + ] + ) + def test_allow_bigger_mode_num_partitions_and_partition_sizes( + self, + num_rows: int, + num_unique_natural_id: int, + group_size: int, + expected_num_unique_natural_ids: List[int], + ) -> None: + """Test allow-bigger mode handles the remainder correctly.""" + dataset = _create_dataset(num_rows, num_unique_natural_id) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=group_size, + mode="allow-bigger", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + # Trigger partitioning + partitions = [ + partitioner.load_partition(i) for i in range(partitioner.num_partitions) + ] + unique_natural_ids = [ + len(partition.unique("natural_id")) for partition in partitions + ] + self.assertEqual(unique_natural_ids, expected_num_unique_natural_ids) + + @parameterized.expand( # type: ignore + # num_rows, num_unique_natural_ids, group_size, expected_num_partitions, + # expected_num_unique_natural_ids + [ + [10, 10, 2, [2, 2, 2, 2, 2]], + [100, 10, 2, [2, 2, 2, 2, 2]], + [12, 6, 3, [3, 3]], + # The cases in which the partitions should be smaller + [10, 7, 2, [2, 2, 2]], + [10, 3, 2, [2]], + ] + ) + def test_drop_reminder_mode_num_partitions_and_partition_sizes( + self, + num_rows: int, + num_unique_natural_id: int, + group_size: int, + expected_num_unique_natural_ids: List[int], + ) -> None: + """Test drop reminder mode.""" + dataset = _create_dataset(num_rows, num_unique_natural_id) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=group_size, + mode="drop-reminder", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + # Trigger partitioning + partitions = [ + partitioner.load_partition(i) for i in range(partitioner.num_partitions) + ] + unique_natural_ids = [ + len(partition.unique("natural_id")) for partition in partitions + ] + self.assertEqual(unique_natural_ids, expected_num_unique_natural_ids) + + @parameterized.expand( # type: ignore + # mode, num_rows, num_unique_natural_ids, group_size + [ + ["strict", 10, 10, 2], + ["allow-smaller", 10, 7, 2], + ["allow-bigger", 10, 7, 2], + ["drop-reminder", 10, 7, 2], + ["strict", 12, 6, 3], + ["allow-smaller", 12, 6, 3], + ["allow-bigger", 12, 6, 3], + ["drop-reminder", 12, 6, 3], + ["allow-smaller", 10, 2, 3], + ] + ) + def test_no_overlapping_natural_ids( + self, + mode: Literal["allow-smaller", "allow-bigger", "drop-reminder", "strict"], + num_rows: int, + num_unique_natural_id: int, + group_size: int, + ) -> None: + """Test that no natural_ids overlap across partitions.""" + dataset = _create_dataset(num_rows, num_unique_natural_id) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=group_size, + mode=mode, + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + + # Trigger partitioning + partitions = [ + partitioner.load_partition(i) for i in range(partitioner.num_partitions) + ] + + # Check for overlaps between partitions + seen_natural_ids: Set[str] = set() + for partition in partitions: + natural_ids_in_partition = set(partition.unique("natural_id")) + + # Check if there is any overlap with previously seen natural IDs + overlap = seen_natural_ids.intersection(natural_ids_in_partition) + self.assertTrue( + len(overlap) == 0, + f"Overlapping natural IDs found between partitions in mode: {mode}. " + f"Overlapping IDs: {overlap}", + ) + + # Add the natural IDs from this partition to the seen set + seen_natural_ids.update(natural_ids_in_partition) + + def test_group_size_bigger_than_num_unique_natural_ids_allow_smaller(self) -> None: + """Test the allow-smaller mode with group size > number of unique natural ids. + + That's the only mode that should work in this scenario. + """ + dataset = _create_dataset(num_rows=10, n_unique_natural_ids=2) + expected_num_unique_natural_ids = [2] + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=3, + mode="allow-smaller", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + # Trigger partitioning + partitions = [ + partitioner.load_partition(i) for i in range(partitioner.num_partitions) + ] + unique_natural_ids = [ + len(partition.unique("natural_id")) for partition in partitions + ] + + self.assertEqual(unique_natural_ids, expected_num_unique_natural_ids) + + def test_strict_mode_with_invalid_group_size(self) -> None: + """Test strict mode raises if group_size does not divide unique IDs evenly.""" + dataset = _create_dataset(num_rows=10, n_unique_natural_ids=3) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=2, + mode="strict", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + with self.assertRaises(ValueError) as context: + _ = partitioner.load_partition(0) + self.assertIn( + "Strict mode requires that the number of unique natural ids is perfectly " + "divisible by the group size.", + str(context.exception), + ) + + def test_too_big_group_size(self) -> None: + """Test raises if the group size > than the number of unique natural ids.""" + n_unique_natural_ids = 3 + dataset = _create_dataset( + num_rows=10, n_unique_natural_ids=n_unique_natural_ids + ) + partitioner = GroupedNaturalIdPartitioner( + partition_by="natural_id", + group_size=n_unique_natural_ids + 1, + mode="allow-bigger", + sort_unique_ids=self.sort_unique_ids, + ) + partitioner.dataset = dataset + with self.assertRaises(ValueError) as context: + _ = partitioner.load_partition(0) + self.assertIn( + "The group size needs to be smaller than the number of the unique " + "natural ids unless you are using allow-smaller mode which will " + "result in a single partition.", + str(context.exception), + ) + + +if __name__ == "__main__": + unittest.main() From 95b9f116269be4f2e42a9daa10dadf89df0216c5 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Tue, 27 Aug 2024 14:27:28 +0100 Subject: [PATCH 134/188] fix(framework) Update FlowerTune template code (#3997) Co-authored-by: Javier --- src/py/flwr/cli/new/new.py | 16 ++-- .../templates/app/README.flowertune.md.tpl | 22 +++-- .../templates/app/code/flwr_tune/app.py.tpl | 89 ----------------- .../{client.py.tpl => client_app.py.tpl} | 90 ++++++++++-------- .../app/code/flwr_tune/config.yaml.tpl | 34 ------- .../app/code/flwr_tune/dataset.py.tpl | 34 ++++++- .../app/code/flwr_tune/models.py.tpl | 3 - .../app/code/flwr_tune/server.py.tpl | 48 ---------- .../app/code/flwr_tune/server_app.py.tpl | 95 +++++++++++++++++++ .../app/code/flwr_tune/static_config.yaml.tpl | 11 --- .../app/code/flwr_tune/strategy.py.tpl | 64 +++++++++++++ .../app/pyproject.flowertune.toml.tpl | 41 ++++++-- 12 files changed, 297 insertions(+), 250 deletions(-) delete mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl rename src/py/flwr/cli/new/templates/app/code/flwr_tune/{client.py.tpl => client_app.py.tpl} (66%) delete mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl delete mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl delete mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 862244da9158..31da7b4ab9fb 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -187,24 +187,20 @@ def new( "pyproject.toml": {"template": f"app/pyproject.{framework_str}.toml.tpl"}, "README.md": {"template": f"app/README.{framework_str}.md.tpl"}, f"{import_name}/__init__.py": {"template": "app/code/__init__.py.tpl"}, - f"{import_name}/server.py": { - "template": "app/code/flwr_tune/server.py.tpl" + f"{import_name}/server_app.py": { + "template": "app/code/flwr_tune/server_app.py.tpl" }, - f"{import_name}/client.py": { - "template": "app/code/flwr_tune/client.py.tpl" + f"{import_name}/client_app.py": { + "template": "app/code/flwr_tune/client_app.py.tpl" }, - f"{import_name}/app.py": {"template": "app/code/flwr_tune/app.py.tpl"}, f"{import_name}/models.py": { "template": "app/code/flwr_tune/models.py.tpl" }, f"{import_name}/dataset.py": { "template": "app/code/flwr_tune/dataset.py.tpl" }, - f"{import_name}/conf/config.yaml": { - "template": "app/code/flwr_tune/config.yaml.tpl" - }, - f"{import_name}/conf/static_config.yaml": { - "template": "app/code/flwr_tune/static_config.yaml.tpl" + f"{import_name}/strategy.py": { + "template": "app/code/flwr_tune/strategy.py.tpl" }, } diff --git a/src/py/flwr/cli/new/templates/app/README.flowertune.md.tpl b/src/py/flwr/cli/new/templates/app/README.flowertune.md.tpl index 2b59937e4130..4bdc9c779a29 100644 --- a/src/py/flwr/cli/new/templates/app/README.flowertune.md.tpl +++ b/src/py/flwr/cli/new/templates/app/README.flowertune.md.tpl @@ -23,10 +23,12 @@ pip install -e . ## Experimental setup -The dataset is partitioned into $num_clients shards with IID fashion serving as clients. -We randomly sample $fraction_fit clients to be available for each round, -and the federated fine-tuning lasts for `200` rounds. -All settings are defined in `$project_name/conf/static_config.yaml`, which is not allowed to be modified for fair competition if you plan to participated in the [LLM leaderboard](https://flower.ai/benchmarks/llm-leaderboard). +The dataset is divided into $num_clients partitions in an IID fashion, a partition is assigned to each ClientApp. +We randomly sample a fraction ($fraction_fit) of the total nodes to participate in each round, for a total of `200` rounds. +All settings are defined in `pyproject.toml`. + +> [!IMPORTANT] +> Please note that `[tool.flwr.app.config.static]` and `options.num-supernodes` under `[tool.flwr.federations.local-simulation]` are not allowed to be modified for fair competition if you plan to participated in the [LLM leaderboard](https://flower.ai/benchmarks/llm-leaderboard). ## Running the challenge @@ -39,7 +41,7 @@ huggingface-cli login ``` Run the challenge with default config values. -The configs are in `$project_name/conf/config.yaml` and `$project_name/conf/static_config.yaml`, and are loaded automatically. +The configs are defined in `[tool.flwr.app.config]` entry of `pyproject.toml`, and are loaded automatically. ```bash flwr run @@ -53,4 +55,12 @@ We use Mistral-7B model with 4-bit quantization as default. The estimated VRAM c | :--------: | :--------: | :--------: | :--------: | :--------: | | VRAM | ~25.50 GB | ~17.30 GB | ~22.80 GB | ~17.40 GB | -You can adjust the CPU/GPU resources you assign to each of the clients based on your device, which is specified with `flower.engine.simulation` in `pyproject.toml`. +You can adjust the CPU/GPU resources you assign to each of the clients based on your device, which are specified with `options.backend.clientapp-cpus` and `options.backend.clientapp-gpus` under `[tool.flwr.federations.local-simulation]` entry in `pyproject.toml`. + + +## Model saving + +The global PEFT model checkpoints are saved every 5 rounds after aggregation on the sever side as default, which can be specified with `train.save-every-round` under [tool.flwr.app.config] entry in `pyproject.toml`. + +> [!NOTE] +> Please provide the last PEFT checkpoint if you plan to participated in the [LLM leaderboard](https://flower.ai/benchmarks/llm-leaderboard). diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl deleted file mode 100644 index 637658c5b23c..000000000000 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +++ /dev/null @@ -1,89 +0,0 @@ -"""$project_name: A Flower / FlowerTune app.""" - -import os -import warnings -from datetime import datetime - -from flwr_datasets import FederatedDataset -from hydra import compose, initialize -from hydra.utils import instantiate - -from flwr.client import ClientApp -from flwr.common import Context, ndarrays_to_parameters -from flwr.server import ServerApp, ServerAppComponents, ServerConfig - -from $import_name.client_app import gen_client_fn, get_parameters -from $import_name.dataset import get_tokenizer_and_data_collator_and_propt_formatting -from $import_name.models import get_model -from $import_name.server_app import fit_weighted_average, get_evaluate_fn, get_on_fit_config - -# Avoid warnings -warnings.filterwarnings("ignore", category=UserWarning) -os.environ["TOKENIZERS_PARALLELISM"] = "true" -os.environ["RAY_DISABLE_DOCKER_CPU_WARNING"] = "1" - -# Initialise regular config -with initialize(config_path="conf", version_base="1.1"): - cfg = compose(config_name="config") - -# Initialise static config -with initialize(config_path="conf", version_base="1.1"): - cfg_static = compose(config_name="static_config") - -cfg.train.num_rounds = cfg_static.num_rounds - -# Create output directory given current timestamp -current_time = datetime.now() -folder_name = current_time.strftime("%Y-%m-%d_%H-%M-%S") -save_path = os.path.join(os.getcwd(), f"results/{folder_name}") -os.makedirs(save_path, exist_ok=True) - -# Partition dataset and get dataloaders -partitioner = instantiate(cfg_static.partitioner) -fds = FederatedDataset( - dataset=cfg_static.dataset.name, partitioners={"train": partitioner} -) -( - tokenizer, - data_collator, - formatting_prompts_func, -) = get_tokenizer_and_data_collator_and_propt_formatting(cfg.model.name) - -# ClientApp for Flower Next -client = ClientApp( - client_fn=gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - cfg.model, - cfg.train, - save_path, - ), -) - -# Get initial model weights -init_model = get_model(cfg.model) -init_model_parameters = get_parameters(init_model) -init_model_parameters = ndarrays_to_parameters(init_model_parameters) - -def server_fn(context: Context): - # Instantiate strategy according to config. Here we pass other arguments - # that are only defined at runtime. - strategy = instantiate( - cfg.strategy, - on_fit_config_fn=get_on_fit_config(), - fit_metrics_aggregation_fn=fit_weighted_average, - initial_parameters=init_model_parameters, - evaluate_fn=get_evaluate_fn( - cfg.model, cfg.train.save_every_round, cfg_static.num_rounds, save_path - ), - ) - - config = ServerConfig(num_rounds=cfg_static.num_rounds) - - return ServerAppComponents(strategy=strategy, config=config) - - -# ServerApp for Flower Next -server = ServerApp(server_fn=server_fn) diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl similarity index 66% rename from src/py/flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl rename to src/py/flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl index 2472e23ece44..19d1e20baccd 100644 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl @@ -1,20 +1,32 @@ """$project_name: A Flower / FlowerTune app.""" +import os +import warnings from collections import OrderedDict -from typing import Callable, Dict, Tuple +from typing import Dict, Tuple import torch +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context +from flwr.common.config import unflatten_dict +from flwr.common.typing import NDArrays, Scalar from omegaconf import DictConfig from peft import get_peft_model_state_dict, set_peft_model_state_dict from transformers import TrainingArguments from trl import SFTTrainer -from flwr.client import NumPyClient -from flwr.common import Context -from flwr.common.typing import NDArrays, Scalar -from $import_name.dataset import reformat +from $import_name.dataset import ( + get_tokenizer_and_data_collator_and_propt_formatting, + load_data, + replace_keys, +) from $import_name.models import cosine_annealing, get_model +# Avoid warnings +os.environ["TOKENIZERS_PARALLELISM"] = "true" +os.environ["RAY_DISABLE_DOCKER_CPU_WARNING"] = "1" +warnings.filterwarnings("ignore", category=UserWarning) + # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes @@ -29,7 +41,7 @@ class FlowerClient(NumPyClient): tokenizer, formatting_prompts_func, data_collator, - save_path, + num_rounds, ): # pylint: disable=too-many-arguments self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") self.train_cfg = train_cfg @@ -37,13 +49,12 @@ class FlowerClient(NumPyClient): self.tokenizer = tokenizer self.formatting_prompts_func = formatting_prompts_func self.data_collator = data_collator - self.save_path = save_path + self.num_rounds = num_rounds + self.trainset = trainset # instantiate model self.model = get_model(model_cfg) - self.trainset = trainset - def fit( self, parameters: NDArrays, config: Dict[str, Scalar] ) -> Tuple[NDArrays, int, Dict]: @@ -52,13 +63,13 @@ class FlowerClient(NumPyClient): new_lr = cosine_annealing( int(config["current_round"]), - self.train_cfg.num_rounds, + self.num_rounds, self.train_cfg.learning_rate_max, self.train_cfg.learning_rate_min, ) self.training_argumnets.learning_rate = new_lr - self.training_argumnets.output_dir = self.save_path + self.training_argumnets.output_dir = config["save_path"] # Construct trainer trainer = SFTTrainer( @@ -95,32 +106,31 @@ def get_parameters(model) -> NDArrays: return [val.cpu().numpy() for _, val in state_dict.items()] -def gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - model_cfg: DictConfig, - train_cfg: DictConfig, - save_path: str, -) -> Callable[[Context], FlowerClient]: # pylint: disable=too-many-arguments - """Generate the client function that creates the Flower Clients.""" - - def client_fn(context: Context) -> FlowerClient: - """Create a Flower client representing a single organization.""" - # Let's get the partition corresponding to the i-th client - partition_id = context.node_config["partition-id"] - client_trainset = fds.load_partition(partition_id, "train") - client_trainset = reformat(client_trainset, llm_task="$llm_challenge_str") - - return FlowerClient( - model_cfg, - train_cfg, - client_trainset, - tokenizer, - formatting_prompts_func, - data_collator, - save_path, - ).to_client() - - return client_fn +def client_fn(context: Context) -> FlowerClient: + """Create a Flower client representing a single organization.""" + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + num_rounds = context.run_config["num-server-rounds"] + cfg = DictConfig(replace_keys(unflatten_dict(context.run_config))) + + # Let's get the client partition + client_trainset = load_data(partition_id, num_partitions, cfg.static.dataset.name) + ( + tokenizer, + data_collator, + formatting_prompts_func, + ) = get_tokenizer_and_data_collator_and_propt_formatting(cfg.model.name) + + return FlowerClient( + cfg.model, + cfg.train, + client_trainset, + tokenizer, + formatting_prompts_func, + data_collator, + num_rounds, + ).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn) diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl deleted file mode 100644 index 9f700dd5b8da..000000000000 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +++ /dev/null @@ -1,34 +0,0 @@ -# Federated Instruction Tuning ---- -model: - name: "mistralai/Mistral-7B-v0.3" - quantization: 4 # 8 or 4 if you want to do quantization with BitsAndBytes - gradient_checkpointing: True - lora: - peft_lora_r: 32 - peft_lora_alpha: 64 - -train: - num_rounds: null - save_every_round: 5 - learning_rate_max: 5e-5 - learning_rate_min: 1e-6 - seq_length: 512 - training_arguments: - output_dir: null # to be set by hydra - learning_rate: null # to be set by the client - per_device_train_batch_size: 16 - gradient_accumulation_steps: 1 - logging_steps: 10 - num_train_epochs: 3 - max_steps: 10 - report_to: null - save_steps: 1000 - save_total_limit: 10 - gradient_checkpointing: True - lr_scheduler_type: "constant" - -strategy: - _target_: flwr.server.strategy.FedAvg - fraction_fit: $fraction_fit - fraction_evaluate: 0.0 # no client evaluation diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl index 1b3691d7cf3c..41381ef7c7a3 100644 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl @@ -1,8 +1,12 @@ """$project_name: A Flower / FlowerTune app.""" +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner from transformers import AutoTokenizer from trl import DataCollatorForCompletionOnlyLM +FDS = None # Cache FederatedDataset + def formatting_prompts_func(example): """Construct prompts.""" @@ -24,7 +28,6 @@ def formatting_prompts_func(example): def get_tokenizer_and_data_collator_and_propt_formatting(model_name: str): """Get tokenizer, data_collator and prompt formatting.""" - # From: https://huggingface.co/docs/trl/en/sft_trainer tokenizer = AutoTokenizer.from_pretrained( model_name, use_fast=True, padding_side="right" ) @@ -49,9 +52,36 @@ def formatting(dataset): def reformat(dataset, llm_task): """Reformat datasets.""" dataset = dataset.rename_column("output", "response") - if llm_task == "finance" or llm_task == "code": + if llm_task in ["finance", "code"]: dataset = dataset.map(formatting, remove_columns=["input"]) if llm_task == "medical": dataset = dataset.remove_columns(["instruction"]) dataset = dataset.rename_column("input", "instruction") return dataset + + +def load_data(partition_id: int, num_partitions: int, dataset_name: str): + """Load partition data.""" + # Only initialize `FederatedDataset` once + global FDS + if FDS is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + FDS = FederatedDataset( + dataset=dataset_name, + partitioners={"train": partitioner}, + ) + client_trainset = FDS.load_partition(partition_id, "train") + client_trainset = reformat(client_trainset, llm_task="generalnlp") + return client_trainset + + +def replace_keys(input_dict, match="-", target="_"): + """Recursively replace match string with target string in dictionary keys.""" + new_dict = {} + for key, value in input_dict.items(): + new_key = key.replace(match, target) + if isinstance(value, dict): + new_dict[new_key] = replace_keys(value, match, target) + else: + new_dict[new_key] = value + return new_dict diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl index a2794f35518c..a548ba9abeef 100644 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl @@ -22,9 +22,6 @@ def cosine_annealing( def get_model(model_cfg: DictConfig): """Load model with appropriate quantization config and other optimizations. - - Please refer to this example for `peft + BitsAndBytes`: - https://github.com/huggingface/peft/blob/main/examples/fp4_finetuning/finetune_fp4_opt_bnb_peft.py """ if model_cfg.quantization == 4: quantization_config = BitsAndBytesConfig(load_in_4bit=True) diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl deleted file mode 100644 index 5dd4d881f2f1..000000000000 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +++ /dev/null @@ -1,48 +0,0 @@ -"""$project_name: A Flower / FlowerTune app.""" - -from $import_name.client_app import set_parameters -from $import_name.models import get_model - - -# Get function that will be executed by the strategy's evaluate() method -# Here we use it to save global model checkpoints -def get_evaluate_fn(model_cfg, save_every_round, total_round, save_path): - """Return an evaluation function for saving global model.""" - - def evaluate(server_round: int, parameters, config): - # Save model - if server_round != 0 and ( - server_round == total_round or server_round % save_every_round == 0 - ): - # Init model - model = get_model(model_cfg) - set_parameters(model, parameters) - - model.save_pretrained(f"{save_path}/peft_{server_round}") - - return 0.0, {} - - return evaluate - - -def get_on_fit_config(): - """ - Return a function that will be used to construct the config - that the client's fit() method will receive. - """ - - def fit_config_fn(server_round: int): - fit_config = {"current_round": server_round} - return fit_config - - return fit_config_fn - - -def fit_weighted_average(metrics): - """Aggregate (federated) evaluation metrics.""" - # Multiply accuracy of each client by number of examples used - losses = [num_examples * m["train_loss"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"train_loss": sum(losses) / sum(examples)} diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl new file mode 100644 index 000000000000..586b929be06c --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl @@ -0,0 +1,95 @@ +"""$project_name: A Flower / FlowerTune app.""" + +import os +from datetime import datetime + +from flwr.common import Context, ndarrays_to_parameters +from flwr.common.config import unflatten_dict +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from omegaconf import DictConfig + +from $import_name.client_app import get_parameters, set_parameters +from $import_name.models import get_model +from $import_name.dataset import replace_keys +from $import_name.strategy import FlowerTuneLlm + + +# Get function that will be executed by the strategy's evaluate() method +# Here we use it to save global model checkpoints +def get_evaluate_fn(model_cfg, save_every_round, total_round, save_path): + """Return an evaluation function for saving global model.""" + + def evaluate(server_round: int, parameters, config): + # Save model + if server_round != 0 and ( + server_round == total_round or server_round % save_every_round == 0 + ): + # Init model + model = get_model(model_cfg) + set_parameters(model, parameters) + + model.save_pretrained(f"{save_path}/peft_{server_round}") + + return 0.0, {} + + return evaluate + + +def get_on_fit_config(save_path): + """Return a function that will be used to construct the config that the + client's fit() method will receive.""" + + def fit_config_fn(server_round: int): + fit_config = {} + fit_config["current_round"] = server_round + fit_config["save_path"] = save_path + return fit_config + + return fit_config_fn + + +def fit_weighted_average(metrics): + """Aggregate (federated) evaluation metrics.""" + # Multiply accuracy of each client by number of examples used + losses = [num_examples * m["train_loss"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"train_loss": sum(losses) / sum(examples)} + + +def server_fn(context: Context): + """Construct components that set the ServerApp behaviour.""" + # Create output directory given current timestamp + current_time = datetime.now() + folder_name = current_time.strftime("%Y-%m-%d_%H-%M-%S") + save_path = os.path.join(os.getcwd(), f"results/{folder_name}") + os.makedirs(save_path, exist_ok=True) + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + cfg = DictConfig(replace_keys(unflatten_dict(context.run_config))) + + # Get initial model weights + init_model = get_model(cfg.model) + init_model_parameters = get_parameters(init_model) + init_model_parameters = ndarrays_to_parameters(init_model_parameters) + + # Define strategy + strategy = FlowerTuneLlm( + fraction_fit=cfg.strategy.fraction_fit, + fraction_evaluate=cfg.strategy.fraction_evaluate, + on_fit_config_fn=get_on_fit_config(save_path), + fit_metrics_aggregation_fn=fit_weighted_average, + initial_parameters=init_model_parameters, + evaluate_fn=get_evaluate_fn( + cfg.model, cfg.train.save_every_round, num_rounds, save_path + ), + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Flower ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl deleted file mode 100644 index a8a4039fc831..000000000000 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +++ /dev/null @@ -1,11 +0,0 @@ -# Federated Instruction Tuning (static) ---- -dataset: - name: $dataset_name - -# FL experimental settings -num_clients: $num_clients # total number of clients -num_rounds: 200 -partitioner: - _target_: flwr_datasets.partitioner.IidPartitioner - num_partitions: $num_clients diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl new file mode 100644 index 000000000000..55b2ee3c9aa7 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl @@ -0,0 +1,64 @@ +"""$project_name: A Flower / FlowerTune app.""" + +from logging import INFO, WARN +from typing import List, Tuple, Union + +from flwr.common import FitIns, FitRes, Parameters, log, parameters_to_ndarrays +from flwr.server.client_manager import ClientManager +from flwr.server.client_proxy import ClientProxy +from flwr.server.strategy import FedAvg + + +class FlowerTuneLlm(FedAvg): + """Customised FedAvg strategy implementation.""" + + def configure_fit( + self, server_round: int, parameters: Parameters, client_manager: ClientManager + ): + """Configure the next round of training.""" + return_clients = super().configure_fit(server_round, parameters, client_manager) + + # Test communication costs + fit_ins_list = [fit_ins for _, fit_ins in return_clients] + test_communication_costs(fit_ins_list) + + return return_clients + + def aggregate_fit( + self, + server_round: int, + results: List[Tuple[ClientProxy, FitRes]], + failures: List[Union[Tuple[ClientProxy, FitRes], BaseException]], + ): + """Aggregate fit results using weighted average.""" + # Test communication costs + fit_res_list = [fit_res for _, fit_res in results] + test_communication_costs(fit_res_list) + + parameters_aggregated, metrics_aggregated = super().aggregate_fit( + server_round, results, failures + ) + + return parameters_aggregated, metrics_aggregated + + +def test_communication_costs(fit_list: List[Union[FitIns, FitRes]]): + """Test communication costs per FL round.""" + + def compute_bytes(weights): + return sum(ele.nbytes for ele in weights) + + size_bytes_list = [ + compute_bytes(parameters_to_ndarrays(fit_ele.parameters)) + for fit_ele in fit_list + ] + comm_cost = 2 * sum(size_bytes_list) / 1024**2 + log(INFO, "Communication costs per round: %f MB", comm_cost) + + if comm_cost > 500: + log( + WARN, + "The total communication costs per round exceed 500 MB. " + "Please consider reducing it if you plan to participate " + "FlowerTune LLM Leaderboard.", + ) diff --git a/src/py/flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl index b564a66090d2..5046a6f89f27 100644 --- a/src/py/flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +++ b/src/py/flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl @@ -8,15 +8,15 @@ version = "1.0.0" description = "" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "flwr-datasets>=0.1.0,<1.0.0", - "hydra-core==1.3.2", + "flwr[simulation]>=1.10.0", + "flwr-datasets>=0.3.0", "trl==0.8.1", "bitsandbytes==0.43.0", "scipy==1.13.0", "peft==0.6.2", "transformers==4.39.3", "sentencepiece==0.2.0", + "omegaconf==2.3.0", ] [tool.hatch.build.targets.wheel] @@ -26,14 +26,41 @@ packages = ["."] publisher = "$username" [tool.flwr.app.components] -serverapp = "$import_name.app:server" -clientapp = "$import_name.app:client" +serverapp = "$import_name.server_app:app" +clientapp = "$import_name.client_app:app" [tool.flwr.app.config] -num-server-rounds = 3 +model.name = "mistralai/Mistral-7B-v0.3" +model.quantization = 4 +model.gradient-checkpointing = true +model.lora.peft-lora-r = 32 +model.lora.peft-lora-alpha = 64 +train.save-every-round = 5 +train.learning-rate-max = 5e-5 +train.learning-rate-min = 1e-6 +train.seq-length = 512 +train.training-arguments.output-dir = "" +train.training-arguments.learning-rate = "" +train.training-arguments.per-device-train-batch-size = 16 +train.training-arguments.gradient-accumulation-steps = 1 +train.training-arguments.logging-steps = 10 +train.training-arguments.num-train-epochs = 3 +train.training-arguments.max-steps = 10 +train.training-arguments.save-steps = 1000 +train.training-arguments.save-total-limit = 10 +train.training-arguments.gradient-checkpointing = true +train.training-arguments.lr-scheduler-type = "constant" +strategy.fraction-fit = $fraction_fit +strategy.fraction-evaluate = 0.0 +num-server-rounds = 200 + +[tool.flwr.app.config.static] +dataset.name = "$dataset_name" [tool.flwr.federations] default = "local-simulation" [tool.flwr.federations.local-simulation] -options.num-supernodes = 10 +options.num-supernodes = $num_clients +options.backend.client-resources.num-cpus = 6 +options.backend.client-resources.num-gpus = 1.0 From 01db3c20e6256af44a2b2bd781719f811fec33e7 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Tue, 27 Aug 2024 15:42:10 +0200 Subject: [PATCH 135/188] fix(framework:skip) Use `app` directory as root for certs (#3850) --- src/py/flwr/cli/run/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index b2c4dc4151cd..6375e71522de 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -124,14 +124,14 @@ def run( def _run_with_superexec( - app: Optional[Path], + app: Path, federation_config: Dict[str, Any], config_overrides: Optional[List[str]], ) -> None: insecure_str = federation_config.get("insecure") if root_certificates := federation_config.get("root-certificates"): - root_certificates_bytes = Path(root_certificates).read_bytes() + root_certificates_bytes = (app / root_certificates).read_bytes() if insecure := bool(insecure_str): typer.secho( "❌ `root_certificates` were provided but the `insecure` parameter" From ebf0d3fcaae1cdade98740aae7f2bfa02b5f6634 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Tue, 27 Aug 2024 17:39:44 +0100 Subject: [PATCH 136/188] feat(framework) Update communication cost calculator for LLM leaderboard (#4013) Co-authored-by: jafermarq Co-authored-by: Daniel J. Beutel --- .../app/code/flwr_tune/strategy.py.tpl | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl b/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl index 55b2ee3c9aa7..8accd70c4e76 100644 --- a/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl @@ -1,5 +1,6 @@ """$project_name: A Flower / FlowerTune app.""" +from io import BytesIO from logging import INFO, WARN from typing import List, Tuple, Union @@ -10,7 +11,14 @@ from flwr.server.strategy import FedAvg class FlowerTuneLlm(FedAvg): - """Customised FedAvg strategy implementation.""" + """Customised FedAvg strategy implementation. + + This class behaves just like FedAvg but also tracks the communication + costs associated with `fit` over FL rounds. + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.comm_tracker = CommunicationTracker() def configure_fit( self, server_round: int, parameters: Parameters, client_manager: ClientManager @@ -20,7 +28,7 @@ class FlowerTuneLlm(FedAvg): # Test communication costs fit_ins_list = [fit_ins for _, fit_ins in return_clients] - test_communication_costs(fit_ins_list) + self.comm_tracker.track(fit_ins_list) return return_clients @@ -33,7 +41,7 @@ class FlowerTuneLlm(FedAvg): """Aggregate fit results using weighted average.""" # Test communication costs fit_res_list = [fit_res for _, fit_res in results] - test_communication_costs(fit_res_list) + self.comm_tracker.track(fit_res_list) parameters_aggregated, metrics_aggregated = super().aggregate_fit( server_round, results, failures @@ -42,23 +50,34 @@ class FlowerTuneLlm(FedAvg): return parameters_aggregated, metrics_aggregated -def test_communication_costs(fit_list: List[Union[FitIns, FitRes]]): - """Test communication costs per FL round.""" +class CommunicationTracker: + """Communication costs tracker over FL rounds.""" + def __init__(self): + self.curr_comm_cost = 0.0 - def compute_bytes(weights): - return sum(ele.nbytes for ele in weights) + @staticmethod + def _compute_bytes(parameters): + return sum([BytesIO(t).getbuffer().nbytes for t in parameters.tensors]) - size_bytes_list = [ - compute_bytes(parameters_to_ndarrays(fit_ele.parameters)) - for fit_ele in fit_list - ] - comm_cost = 2 * sum(size_bytes_list) / 1024**2 - log(INFO, "Communication costs per round: %f MB", comm_cost) + def track(self, fit_list: List[Union[FitIns, FitRes]]): + size_bytes_list = [ + self._compute_bytes(fit_ele.parameters) + for fit_ele in fit_list + ] + comm_cost = sum(size_bytes_list) / 1024**2 - if comm_cost > 500: + self.curr_comm_cost += comm_cost log( - WARN, - "The total communication costs per round exceed 500 MB. " - "Please consider reducing it if you plan to participate " - "FlowerTune LLM Leaderboard.", + INFO, + "Communication budget: used %.2f MB (+%.2f MB this round) / 200,000 MB", + self.curr_comm_cost, + comm_cost, ) + + if self.curr_comm_cost > 2e5: + log( + WARN, + "The accumulated communication cost has exceeded 200,000 MB. " + "Please consider reducing it if you plan to participate " + "FlowerTune LLM Leaderboard.", + ) From 5b3784a099f2f285e7ccb657c8457a2f83d0d8a8 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Tue, 27 Aug 2024 18:05:58 +0100 Subject: [PATCH 137/188] refactor(framework) Update FlowerTune LLM example (#4046) Co-authored-by: jafermarq --- README.md | 2 +- examples/doc/source/conf.py | 1 + examples/flowertune-llm/README.md | 118 ++++++++++++++ .../_static/train_loss_smooth.png | Bin .../flowertune-llm/flowertune_llm/__init__.py | 1 + .../flowertune_llm/client_app.py} | 119 ++++++++------- .../flowertune_llm}/dataset.py | 33 ++++ .../flowertune_llm}/models.py | 5 +- .../flowertune_llm/server_app.py | 95 ++++++++++++ examples/flowertune-llm/pyproject.toml | 66 ++++++++ .../test.py | 0 examples/llm-flowertune/README.md | 144 ------------------ examples/llm-flowertune/app.py | 85 ----------- examples/llm-flowertune/conf/config.yaml | 45 ------ examples/llm-flowertune/main.py | 92 ----------- examples/llm-flowertune/requirements.txt | 10 -- examples/llm-flowertune/utils.py | 43 ------ 17 files changed, 384 insertions(+), 475 deletions(-) create mode 100644 examples/flowertune-llm/README.md rename examples/{llm-flowertune => flowertune-llm}/_static/train_loss_smooth.png (100%) create mode 100644 examples/flowertune-llm/flowertune_llm/__init__.py rename examples/{llm-flowertune/client.py => flowertune-llm/flowertune_llm/client_app.py} (55%) rename examples/{llm-flowertune => flowertune-llm/flowertune_llm}/dataset.py (53%) rename examples/{llm-flowertune => flowertune-llm/flowertune_llm}/models.py (96%) create mode 100644 examples/flowertune-llm/flowertune_llm/server_app.py create mode 100644 examples/flowertune-llm/pyproject.toml rename examples/{llm-flowertune => flowertune-llm}/test.py (100%) delete mode 100644 examples/llm-flowertune/README.md delete mode 100644 examples/llm-flowertune/app.py delete mode 100644 examples/llm-flowertune/conf/config.yaml delete mode 100644 examples/llm-flowertune/main.py delete mode 100644 examples/llm-flowertune/requirements.txt delete mode 100644 examples/llm-flowertune/utils.py diff --git a/README.md b/README.md index 3f1d96ca53c0..c36e012d5644 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Other [examples](https://github.com/adap/flower/tree/main/examples): - [PyTorch: From Centralized to Federated](https://github.com/adap/flower/tree/main/examples/pytorch-from-centralized-to-federated) - [Vertical FL](https://github.com/adap/flower/tree/main/examples/vertical-fl) - [Federated Finetuning of OpenAI's Whisper](https://github.com/adap/flower/tree/main/examples/whisper-federated-finetuning) -- [Federated Finetuning of Large Language Model](https://github.com/adap/flower/tree/main/examples/llm-flowertune) +- [Federated Finetuning of Large Language Model](https://github.com/adap/flower/tree/main/examples/flowertune-llm) - [Federated Finetuning of a Vision Transformer](https://github.com/adap/flower/tree/main/examples/flowertune-vit) - [Advanced Flower with TensorFlow/Keras](https://github.com/adap/flower/tree/main/examples/advanced-tensorflow) - [Advanced Flower with PyTorch](https://github.com/adap/flower/tree/main/examples/advanced-pytorch) diff --git a/examples/doc/source/conf.py b/examples/doc/source/conf.py index 2c2dd2742633..7712aa5f4f59 100644 --- a/examples/doc/source/conf.py +++ b/examples/doc/source/conf.py @@ -66,6 +66,7 @@ "quickstart-mxnet": "index.html", "mxnet-from-centralized-to-federated": "index.html", "app-secure-aggregation": "flower-secure-aggregation.html", + "llm-flowertune": "flowertune-llm.html", } diff --git a/examples/flowertune-llm/README.md b/examples/flowertune-llm/README.md new file mode 100644 index 000000000000..51cae73ae88a --- /dev/null +++ b/examples/flowertune-llm/README.md @@ -0,0 +1,118 @@ +--- +tags: [llm, nlp, LLama] +dataset: [Alpaca-GPT4] +framework: [PEFT, torch] +--- + +# FlowerTune LLM: Federated LLM Fine-tuning with Flower + +Large language models (LLMs), which have been trained on vast amounts of publicly accessible data, have shown remarkable effectiveness in a wide range of areas. +However, despite the fact that more data typically leads to improved performance, there is a concerning prospect that the supply of high-quality public data will deplete within a few years. +Federated LLM training could unlock access to an endless pool of distributed private data by allowing multiple data owners to collaboratively train a shared model without the need to exchange raw data. + +This introductory example conducts federated instruction tuning with pretrained [OpenLLaMA](https://huggingface.co/openlm-research) models on [Alpaca-GPT4](https://huggingface.co/datasets/vicgalle/alpaca-gpt4) dataset. +We implement FlowerTune LLM by integrating a bundle of techniques: 1) We use [Flower Datasets](https://flower.dev/docs/datasets/) to download, partition and preprocess the dataset. 2) The fine-tuning is done using the [πŸ€—PEFT](https://huggingface.co/docs/peft/en/index) library. 3) We use Flower's Simulation Engine to simulate the LLM fine-tuning process in federated way, +which allows users to perform the training on a single GPU. + +## Set up the project + +Start by cloning the example project: + +```shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/flowertune-llm . \ + && rm -rf _tmp \ + && cd flowertune-llm +``` + +This will create a new directory called `flowertune-llm` with the following structure: + +```shell +flowertune-llm +β”œβ”€β”€ flowertune_llm +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ β”œβ”€β”€ dataset.py # Defines your dataset and tokenizer +β”‚ └── models.py # Defines your models +β”‚ +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +β”œβ”€β”€ test.py # Test pre-trained model +└── README.md +``` + +### Install dependencies and project + +Install the dependencies defined in `pyproject.toml` as well as the `flowertune_llm` package. + +```bash +pip install -e . +``` + +## Run the project + +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. + +### Run with the Simulation Engine + +```bash +flwr run . +``` + +This command will run FL simulations with a 4-bit [OpenLLaMA 3Bv2](https://huggingface.co/openlm-research/open_llama_3b_v2) model involving 2 clients per rounds for 100 FL rounds. You can override configuration parameters directly from the command line. Below are a few settings you might want to test: + +```bash +# Use OpenLLaMA-7B instead of 3B and 8-bits quantization +flwr run . --run-config "model.name='openlm-research/open_llama_7b_v2' model.quantization=8" + +# Run for 50 rounds but increasing the fraction of clients that participate per round to 25% +flwr run . --run-config "num-server-rounds=50 strategy.fraction-fit=0.25" +``` + +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. + +## Expected results + +![](_static/train_loss_smooth.png) + +As expected, OpenLLaMA-7B model works better than its 3B version with lower training loss. With the hyperparameters tested, the 8-bit model seems to deliver lower training loss for the smaller 3B model compared to its 4-bit version. + +## VRAM consumption + +| Models | 7-billion (8-bit) | 7-billion (4-bit) | 3-billion (8-bit) | 3-billion (4-bit) | +| :----: | :---------------: | :---------------: | :---------------: | :---------------: | +| VRAM | ~22.00 GB | ~16.50 GB | ~13.50 GB | ~10.60 GB | + +We make use of the [bitsandbytes](https://huggingface.co/docs/bitsandbytes/main/en/index) library in conjunction with [PEFT](https://huggingface.co/docs/peft/en/index) to derive LLMs that can be fine-tuned efficiently. +The above table shows the VRAM consumption per client for the different models considered in this example. +You can adjust the CPU/GPU resources you assign to each of the clients based on your device. +For example, it is easy to train 2 concurrent clients on each GPU (24 GB VRAM) if you choose 3-billion (4-bit) model. +Assigning 50% of the GPU's VRAM to each client by setting `options.backend.clientapp-gpus = 0.5` under `[tool.flwr.federations.local-simulation]` in `pyproject.toml`. + +## Test with your Questions + +We provide a script to test your trained model by passing your specified questions. For example: + +```bash +python test.py --peft-path=/path/to/trained-model-dir/ \ + --question="What is the ideal 1-day plan in London?" +``` + +An answer generated from federated trained 7-billion (8-bit) OpenLLaMA model: + +``` +Great choice. +London has so much to offer, and you can really soak up all the sights and sounds in just a single day. +Here's a suggested itinerary for you. +Start your day off with a hearty breakfast at an authentic British diner. +Then head to the iconic Big Ben and the Houses of Parliament to learn about the history of the city. +Next, make your way to Westminster Abbey to see the many historical monuments and memorials. +From there, cross the river Thames to the Tower of London, which is home to the Crown Jewels of England and Scotland. +Finally, end your day with a relaxing visit to the London Eye, the tallest Ferris wheel in Europe, for a beautiful view of the city. +``` + +The [`Vicuna`](https://huggingface.co/lmsys/vicuna-13b-v1.1) template we used in this example is for a chat assistant. +The generated answer is expected to be a multi-turn conversations. Feel free to try more interesting questions! diff --git a/examples/llm-flowertune/_static/train_loss_smooth.png b/examples/flowertune-llm/_static/train_loss_smooth.png similarity index 100% rename from examples/llm-flowertune/_static/train_loss_smooth.png rename to examples/flowertune-llm/_static/train_loss_smooth.png diff --git a/examples/flowertune-llm/flowertune_llm/__init__.py b/examples/flowertune-llm/flowertune_llm/__init__.py new file mode 100644 index 000000000000..e786a4d4b73d --- /dev/null +++ b/examples/flowertune-llm/flowertune_llm/__init__.py @@ -0,0 +1 @@ +"""flowertune_llm.""" diff --git a/examples/llm-flowertune/client.py b/examples/flowertune-llm/flowertune_llm/client_app.py similarity index 55% rename from examples/llm-flowertune/client.py rename to examples/flowertune-llm/flowertune_llm/client_app.py index c81333f664b3..992b0f1a3e09 100644 --- a/examples/llm-flowertune/client.py +++ b/examples/flowertune-llm/flowertune_llm/client_app.py @@ -1,21 +1,40 @@ +"""flowertune-llm: A Flower / FlowerTune app.""" + +import os +import warnings +from typing import Dict, Tuple from collections import OrderedDict -from typing import Callable, Dict, Tuple -import flwr as fl import torch +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context +from flwr.common.config import unflatten_dict from flwr.common.typing import NDArrays, Scalar from omegaconf import DictConfig + from peft import get_peft_model_state_dict, set_peft_model_state_dict from transformers import TrainingArguments from trl import SFTTrainer -from models import cosine_annealing, get_model +from flowertune_llm.dataset import ( + get_tokenizer_and_data_collator_and_propt_formatting, + load_data, + replace_keys, +) +from flowertune_llm.models import ( + cosine_annealing, + get_model, +) + +# Avoid warnings +os.environ["TOKENIZERS_PARALLELISM"] = "true" +os.environ["RAY_DISABLE_DOCKER_CPU_WARNING"] = "1" +warnings.filterwarnings("ignore", category=UserWarning) # pylint: disable=too-many-arguments -class FlowerClient( - fl.client.NumPyClient -): # pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes +class FlowerClient(NumPyClient): """Standard Flower client for CNN training.""" def __init__( @@ -26,7 +45,7 @@ def __init__( tokenizer, formatting_prompts_func, data_collator, - save_path, + num_rounds, ): # pylint: disable=too-many-arguments self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") self.train_cfg = train_cfg @@ -34,19 +53,12 @@ def __init__( self.tokenizer = tokenizer self.formatting_prompts_func = formatting_prompts_func self.data_collator = data_collator - self.save_path = save_path + self.num_rounds = num_rounds + self.trainset = trainset # instantiate model self.model = get_model(model_cfg) - self.trainset = trainset - - def get_parameters(self, config: Dict[str, Scalar]) -> NDArrays: - """Return the parameters of the current net.""" - - state_dict = get_peft_model_state_dict(self.model) - return [val.cpu().numpy() for _, val in state_dict.items()] - def fit( self, parameters: NDArrays, config: Dict[str, Scalar] ) -> Tuple[NDArrays, int, Dict]: @@ -55,13 +67,13 @@ def fit( new_lr = cosine_annealing( int(config["current_round"]), - self.train_cfg.num_rounds, + self.num_rounds, self.train_cfg.learning_rate_max, self.train_cfg.learning_rate_min, ) self.training_argumnets.learning_rate = new_lr - self.training_argumnets.output_dir = self.save_path + self.training_argumnets.output_dir = config["save_path"] # Construct trainer trainer = SFTTrainer( @@ -78,7 +90,7 @@ def fit( results = trainer.train() return ( - self.get_parameters({}), + get_parameters(self.model), len(self.trainset), {"train_loss": results.training_loss}, ) @@ -92,38 +104,37 @@ def set_parameters(model, parameters: NDArrays) -> None: set_peft_model_state_dict(model, state_dict) -def gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - model_cfg: DictConfig, - train_cfg: DictConfig, - save_path: str, - partition_id: int = 0, - api: bool = False, -) -> Callable[[str], FlowerClient]: # pylint: disable=too-many-arguments - """Generate the client function that creates the Flower Clients.""" - - def client_fn(cid: str) -> FlowerClient: - """Create a Flower client representing a single organization.""" - - # Let's get the partition corresponding to the i-th client - client_trainset = ( - fds.load_partition(partition_id, "train") - if api - else fds.load_partition(int(cid), "train") - ) - client_trainset = client_trainset.rename_column("output", "response") - - return FlowerClient( - model_cfg, - train_cfg, - client_trainset, - tokenizer, - formatting_prompts_func, - data_collator, - save_path, - ).to_client() - - return client_fn +def get_parameters(model) -> NDArrays: + """Return the parameters of the current net.""" + state_dict = get_peft_model_state_dict(model) + return [val.cpu().numpy() for _, val in state_dict.items()] + + +def client_fn(context: Context) -> FlowerClient: + """Create a Flower client representing a single organization.""" + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + num_rounds = context.run_config["num-server-rounds"] + cfg = DictConfig(replace_keys(unflatten_dict(context.run_config))) + + # Let's get the client partition + client_trainset = load_data(partition_id, num_partitions, cfg.dataset.name) + ( + tokenizer, + data_collator, + formatting_prompts_func, + ) = get_tokenizer_and_data_collator_and_propt_formatting(cfg.model.name) + + return FlowerClient( + cfg.model, + cfg.train, + client_trainset, + tokenizer, + formatting_prompts_func, + data_collator, + num_rounds, + ).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn) diff --git a/examples/llm-flowertune/dataset.py b/examples/flowertune-llm/flowertune_llm/dataset.py similarity index 53% rename from examples/llm-flowertune/dataset.py rename to examples/flowertune-llm/flowertune_llm/dataset.py index 571be31f7fba..87595b3f9ccd 100644 --- a/examples/llm-flowertune/dataset.py +++ b/examples/flowertune-llm/flowertune_llm/dataset.py @@ -1,6 +1,11 @@ from transformers import AutoTokenizer from trl import DataCollatorForCompletionOnlyLM +from flwr_datasets.partitioner import IidPartitioner +from flwr_datasets import FederatedDataset + +FDS = None # Cache FederatedDataset + def formatting_prompts_func(example): output_texts = [] @@ -27,3 +32,31 @@ def get_tokenizer_and_data_collator_and_propt_formatting(model_name: str): ) return tokenizer, data_collator, formatting_prompts_func + + +def load_data(partition_id: int, num_partitions: int, dataset_name: str): + """Load partition data.""" + # Only initialize `FederatedDataset` once + global FDS + if FDS is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + FDS = FederatedDataset( + dataset=dataset_name, + partitioners={"train": partitioner}, + ) + client_trainset = FDS.load_partition(partition_id, "train") + client_trainset = client_trainset.rename_column("output", "response") + + return client_trainset + + +def replace_keys(input_dict, match="-", target="_"): + """Recursively replace match string with target string in dictionary keys.""" + new_dict = {} + for key, value in input_dict.items(): + new_key = key.replace(match, target) + if isinstance(value, dict): + new_dict[new_key] = replace_keys(value, match, target) + else: + new_dict[new_key] = value + return new_dict diff --git a/examples/llm-flowertune/models.py b/examples/flowertune-llm/flowertune_llm/models.py similarity index 96% rename from examples/llm-flowertune/models.py rename to examples/flowertune-llm/flowertune_llm/models.py index f32c800cf2c1..7d0c8f391687 100644 --- a/examples/llm-flowertune/models.py +++ b/examples/flowertune-llm/flowertune_llm/models.py @@ -2,7 +2,10 @@ import torch from omegaconf import DictConfig -from peft import LoraConfig, get_peft_model +from peft import ( + LoraConfig, + get_peft_model, +) from peft.utils import prepare_model_for_kbit_training from transformers import AutoModelForCausalLM, BitsAndBytesConfig diff --git a/examples/flowertune-llm/flowertune_llm/server_app.py b/examples/flowertune-llm/flowertune_llm/server_app.py new file mode 100644 index 000000000000..309166cc30a3 --- /dev/null +++ b/examples/flowertune-llm/flowertune_llm/server_app.py @@ -0,0 +1,95 @@ +"""flowertune-llm: A Flower / FlowerTune app.""" + +import os +from datetime import datetime + +from flwr.common import Context, ndarrays_to_parameters +from flwr.common.config import unflatten_dict +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg +from omegaconf import DictConfig + +from flowertune_llm.models import get_model +from flowertune_llm.dataset import replace_keys +from flowertune_llm.client_app import get_parameters, set_parameters + + +# Get function that will be executed by the strategy's evaluate() method +# Here we use it to save global model checkpoints +def get_evaluate_fn(model_cfg, save_every_round, total_round, save_path): + """Return an evaluation function for saving global model.""" + + def evaluate(server_round: int, parameters, config): + # Save model + if server_round != 0 and ( + server_round == total_round or server_round % save_every_round == 0 + ): + # Init model + model = get_model(model_cfg) + set_parameters(model, parameters) + + model.save_pretrained(f"{save_path}/peft_{server_round}") + + return 0.0, {} + + return evaluate + + +def get_on_fit_config(save_path): + """Return a function that will be used to construct the config that the client's + fit() method will receive.""" + + def fit_config_fn(server_round: int): + fit_config = {} + fit_config["current_round"] = server_round + fit_config["save_path"] = save_path + return fit_config + + return fit_config_fn + + +def fit_weighted_average(metrics): + """Aggregate (federated) evaluation metrics.""" + # Multiply accuracy of each client by number of examples used + losses = [num_examples * m["train_loss"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"train_loss": sum(losses) / sum(examples)} + + +def server_fn(context: Context): + """Construct components that set the ServerApp behaviour.""" + # Create output directory given current timestamp + current_time = datetime.now() + folder_name = current_time.strftime("%Y-%m-%d_%H-%M-%S") + save_path = os.path.join(os.getcwd(), f"results/{folder_name}") + os.makedirs(save_path, exist_ok=True) + + # Read from config + num_rounds = context.run_config["num-server-rounds"] + cfg = DictConfig(replace_keys(unflatten_dict(context.run_config))) + + # Get initial model weights + init_model = get_model(cfg.model) + init_model_parameters = get_parameters(init_model) + init_model_parameters = ndarrays_to_parameters(init_model_parameters) + + # Define strategy + strategy = FedAvg( + fraction_fit=cfg.strategy.fraction_fit, + fraction_evaluate=cfg.strategy.fraction_evaluate, + on_fit_config_fn=get_on_fit_config(save_path), + fit_metrics_aggregation_fn=fit_weighted_average, + initial_parameters=init_model_parameters, + evaluate_fn=get_evaluate_fn( + cfg.model, cfg.train.save_every_round, num_rounds, save_path + ), + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Flower ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/flowertune-llm/pyproject.toml b/examples/flowertune-llm/pyproject.toml new file mode 100644 index 000000000000..8171d7680620 --- /dev/null +++ b/examples/flowertune-llm/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "flowertune-llm" +version = "1.0.0" +description = "FlowerTune LLM: Federated LLM Fine-tuning with Flower" +license = "Apache-2.0" +dependencies = [ + "flwr-nightly[simulation]==1.11.0.dev20240826", + "flwr-datasets>=0.3.0", + "trl==0.8.1", + "bitsandbytes==0.43.0", + "scipy==1.13.0", + "peft==0.6.2", + "fschat[model_worker,webui]==0.2.35", + "transformers==4.39.3", + "sentencepiece==0.2.0", + "omegaconf==2.3.0", + "hf_transfer==0.1.8", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "flowertune_llm.server_app:app" +clientapp = "flowertune_llm.client_app:app" + +[tool.flwr.app.config] +dataset.name = "vicgalle/alpaca-gpt4" +model.name = "openlm-research/open_llama_3b_v2" +model.quantization = 4 +model.gradient-checkpointing = true +model.lora.peft-lora-r = 32 +model.lora.peft-lora-alpha = 64 +train.save-every-round = 5 +train.learning-rate-max = 5e-5 +train.learning-rate-min = 1e-6 +train.seq-length = 512 +train.training-arguments.output-dir = "" +train.training-arguments.learning-rate = "" +train.training-arguments.per-device-train-batch-size = 16 +train.training-arguments.gradient-accumulation-steps = 1 +train.training-arguments.logging-steps = 10 +train.training-arguments.num-train-epochs = 3 +train.training-arguments.max-steps = 10 +train.training-arguments.save-steps = 1000 +train.training-arguments.save-total-limit = 10 +train.training-arguments.gradient-checkpointing = true +train.training-arguments.lr-scheduler-type = "constant" +strategy.fraction-fit = 0.1 +strategy.fraction-evaluate = 0.0 +num-server-rounds = 100 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 20 +options.backend.client-resources.num-cpus = 8 +options.backend.client-resources.num-gpus = 1.0 diff --git a/examples/llm-flowertune/test.py b/examples/flowertune-llm/test.py similarity index 100% rename from examples/llm-flowertune/test.py rename to examples/flowertune-llm/test.py diff --git a/examples/llm-flowertune/README.md b/examples/llm-flowertune/README.md deleted file mode 100644 index 46076e0b2078..000000000000 --- a/examples/llm-flowertune/README.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: Federated LLM Fine-tuning with Flower -tags: [llm, nlp, LLama2] -dataset: [Alpaca-GPT4] -framework: [PEFT, torch] ---- - -# LLM FlowerTune: Federated LLM Fine-tuning with Flower - -Large language models (LLMs), which have been trained on vast amounts of publicly accessible data, have shown remarkable effectiveness in a wide range of areas. -However, despite the fact that more data typically leads to improved performance, there is a concerning prospect that the supply of high-quality public data will deplete within a few years. -Federated LLM training could unlock access to an endless pool of distributed private data by allowing multiple data owners to collaboratively train a shared model without the need to exchange raw data. - -This introductory example conducts federated instruction tuning with pretrained [LLama2](https://huggingface.co/openlm-research) models on [Alpaca-GPT4](https://huggingface.co/datasets/vicgalle/alpaca-gpt4) dataset. -We implement LLM FlowerTune by integrating a bundle of techniques: 1) We use [Flower Datasets](https://flower.dev/docs/datasets/) to download, partition and preprocess the dataset. 2) The fine-tuning is done using the [πŸ€—PEFT](https://huggingface.co/docs/peft/en/index) library. 3) We use Flower's Simulation Engine to simulate the LLM fine-tuning process in federated way, -which allows users to perform the training on a single GPU. - -## Environment Setup - -Start by cloning the code example. We prepared a single-line command that you can copy into your shell which will checkout the example for you: - -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/llm-flowertune . && rm -rf flower && cd llm-flowertune -``` - -This will create a new directory called `llm-flowertune` containing the following files: - -``` --- README.md <- Your're reading this right now --- main.py <- Start fed-LLM simulation --- client.py <- Flower client constructor --- model.py <- Model build --- dataset.py <- Dataset and tokenizer build --- utils.py <- Utility functions --- test.py <- Test pre-trained model --- app.py <- ServerApp/ClientApp for Flower-Next --- conf/config.yaml <- Configuration file --- requirements.txt <- Example dependencies -``` - -### Installing dependencies - -Project dependencies are defined in `requirements.txt`. Install them with: - -```shell -pip install -r requirements.txt -``` - -## Run LLM Fine-tuning - -With an activated Python environment, run the example with default config values. The config is in `conf/config.yaml` and is loaded automatically. - -```bash -# Run with default config -python main.py -``` - -This command will run FL simulations with a 4-bit [OpenLLaMA 7Bv2](https://huggingface.co/openlm-research/open_llama_7b_v2) model involving 2 clients per rounds for 100 FL rounds. You can override configuration parameters directly from the command line. Below are a few settings you might want to test: - -```bash -# Use OpenLLaMA-3B instead of 7B and 8-bits quantization -python main.py model.name="openlm-research/open_llama_3b_v2" model.quantization=8 - -# Run for 50 rounds but increasing the fraction of clients that participate per round to 25% -python main.py num_rounds=50 fraction_fit.fraction_fit=0.25 -``` - -## Expected Results - -![](_static/train_loss_smooth.png) - -As expected, LLama2-7B model works better than its 3B version with lower training loss. With the hyperparameters tested, the 8-bit model seems to deliver lower training loss for the smaller 3B model compared to its 4-bit version. - -You can run all 8 experiments with a single command as: - -```bash -python main.py --multirun model.name="openlm-research/open_llama_7b_v2","openlm-research/open_llama_3b_v2" model.quantization=8,4 strategy.fraction_fit=0.1,0.2 -``` - -## VRAM Consumption - -| Models | 7-billion (8-bit) | 7-billion (4-bit) | 3-billion (8-bit) | 3-billion (4-bit) | -| :----: | :---------------: | :---------------: | :---------------: | :---------------: | -| VRAM | ~22.00 GB | ~16.50 GB | ~13.50 GB | ~10.60 GB | - -We make use of the [bitsandbytes](https://huggingface.co/docs/bitsandbytes/main/en/index) library in conjunction with [PEFT](https://huggingface.co/docs/peft/en/index) to derive LLMs that can be fine-tuned efficiently. -The above table shows the VRAM consumption per client for the different models considered in this example. -You can adjust the CPU/GPU resources you assign to each of the clients based on your device. -For example, it is easy to train 2 concurrent clients on each GPU (24 GB VRAM) if you choose 3-billion (4-bit) model. - -```bash -# This will assign 50% of the GPU's VRAM to each client. -python main.py model.name="openlm-research/open_llama_3b_v2" model.quantization=4 client_resources.num_gpus=0.5 -``` - -## Test with your Questions - -We provide a script to test your trained model by passing your specified questions. For example: - -```bash -python test.py --peft-path=/path/to/trained-model-dir/ \ - --question="What is the ideal 1-day plan in London?" -``` - -An answer generated from federated trained 7-billion (8-bit) LLama2 model: - -``` -Great choice. -London has so much to offer, and you can really soak up all the sights and sounds in just a single day. -Here's a suggested itinerary for you. -Start your day off with a hearty breakfast at an authentic British diner. -Then head to the iconic Big Ben and the Houses of Parliament to learn about the history of the city. -Next, make your way to Westminster Abbey to see the many historical monuments and memorials. -From there, cross the river Thames to the Tower of London, which is home to the Crown Jewels of England and Scotland. -Finally, end your day with a relaxing visit to the London Eye, the tallest Ferris wheel in Europe, for a beautiful view of the city. -``` - -The [`Vicuna`](https://huggingface.co/lmsys/vicuna-13b-v1.1) template we used in this example is for a chat assistant. -The generated answer is expected to be a multi-turn conversations. Feel free to try more interesting questions! - -## Run with Flower Next (preview) - -We conduct a 2-client setting to demonstrate how to run federated LLM fine-tuning with Flower Next. -Please follow the steps below: - -1. Start the long-running Flower server (SuperLink) - ```bash - flower-superlink --insecure - ``` -2. Start the long-running Flower client (SuperNode) - ```bash - # In a new terminal window, start the first long-running Flower client: - flower-client-app app:client1 --insecure - ``` - ```bash - # In another new terminal window, start the second long-running Flower client: - flower-client-app app:client2 --insecure - ``` -3. Run the Flower App - ```bash - # With both the long-running server (SuperLink) and two clients (SuperNode) up and running, - # we can now run the actual Flower App: - flower-server-app app:server --insecure - ``` diff --git a/examples/llm-flowertune/app.py b/examples/llm-flowertune/app.py deleted file mode 100644 index db6595c94d31..000000000000 --- a/examples/llm-flowertune/app.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import warnings - -import flwr as fl -from flwr_datasets import FederatedDataset -from hydra import compose, initialize - -from client import gen_client_fn -from dataset import get_tokenizer_and_data_collator_and_propt_formatting -from utils import fit_weighted_average, get_on_fit_config - -warnings.filterwarnings("ignore", category=UserWarning) - -NUM_ROUNDS = 100 -save_path = "./results/" - -with initialize(config_path="conf"): - cfg = compose(config_name="config") - -# Reset the number of number -cfg.num_rounds = NUM_ROUNDS -cfg.train.num_rounds = NUM_ROUNDS - -# Create output directory -if not os.path.exists(save_path): - os.mkdir(save_path) - -# Partition dataset and get dataloaders -# We set the number of partitions to 20 for fast processing. -fds = FederatedDataset( - dataset=cfg.dataset.name, partitioners={"train": cfg.num_clients} -) -( - tokenizer, - data_collator, - formatting_prompts_func, -) = get_tokenizer_and_data_collator_and_propt_formatting(cfg.model.name) - - -# ClientApp for client #1 (Flower Next) -client1 = fl.client.ClientApp( - client_fn=gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - cfg.model, - cfg.train, - save_path, - partition_id=0, - api=True, - ), -) - - -# ClientApp for client #2 (Flower Next) -client2 = fl.client.ClientApp( - client_fn=gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - cfg.model, - cfg.train, - save_path, - partition_id=1, - api=True, - ), -) - - -# Instantiate strategy. -strategy = fl.server.strategy.FedAvg( - min_available_clients=2, # Simulate a 2-client setting - fraction_fit=1.0, - fraction_evaluate=0.0, # no client evaluation - on_fit_config_fn=get_on_fit_config(), - fit_metrics_aggregation_fn=fit_weighted_average, -) - -# ServerApp for Flower-Next -server = fl.server.ServerApp( - config=fl.server.ServerConfig(num_rounds=NUM_ROUNDS), - strategy=strategy, -) diff --git a/examples/llm-flowertune/conf/config.yaml b/examples/llm-flowertune/conf/config.yaml deleted file mode 100644 index 0b769d351479..000000000000 --- a/examples/llm-flowertune/conf/config.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Federated Instruction Tuning on General Dataset ---- - -num_clients: 20 # total number of clients -num_rounds: 100 - -dataset: - name: "vicgalle/alpaca-gpt4" - -model: - name: "openlm-research/open_llama_7b_v2" - quantization: 4 # 8 or 4 if you want to do quantization with BitsAndBytes - gradient_checkpointing: True - lora: - peft_lora_r: 32 - peft_lora_alpha: 64 - -train: - num_rounds: ${num_rounds} - save_every_round: 5 - learning_rate_max: 5e-5 - learning_rate_min: 1e-6 - seq_length: 512 - training_arguments: - output_dir: null # to be set by hydra - learning_rate: null # to be set by the client - per_device_train_batch_size: 16 - gradient_accumulation_steps: 1 - logging_steps: 10 - num_train_epochs: 3 - max_steps: 10 - report_to: null - save_steps: 1000 - save_total_limit: 10 - gradient_checkpointing: ${model.gradient_checkpointing} - lr_scheduler_type: "constant" - -strategy: - _target_: flwr.server.strategy.FedAvg - fraction_fit: 0.1 # sample 10% of clients (i.e. 2 per round) - fraction_evaluate: 0.0 # no client evaluation - -client_resources: - num_cpus: 8 - num_gpus: 1.0 diff --git a/examples/llm-flowertune/main.py b/examples/llm-flowertune/main.py deleted file mode 100644 index ec8308601efb..000000000000 --- a/examples/llm-flowertune/main.py +++ /dev/null @@ -1,92 +0,0 @@ -import pickle -import warnings - -import flwr as fl -import hydra -from flwr_datasets import FederatedDataset -from hydra.core.hydra_config import HydraConfig -from hydra.utils import instantiate -from omegaconf import DictConfig, OmegaConf - -from client import gen_client_fn -from dataset import get_tokenizer_and_data_collator_and_propt_formatting -from utils import fit_weighted_average, get_evaluate_fn, get_on_fit_config - -warnings.filterwarnings("ignore", category=UserWarning) - - -@hydra.main(config_path="conf", config_name="config", version_base=None) -def main(cfg: DictConfig) -> None: - """Run federated LLM fine-tuning. - - Parameters - ---------- - cfg : DictConfig - An omegaconf object that stores the hydra config. - """ - # Print config structured as YAML - print(OmegaConf.to_yaml(cfg)) - - # Partition dataset and get dataloaders - fds = FederatedDataset( - dataset=cfg.dataset.name, partitioners={"train": cfg.num_clients} - ) - ( - tokenizer, - data_collator, - formatting_prompts_func, - ) = get_tokenizer_and_data_collator_and_propt_formatting( - cfg.model.name, - ) - - # Hydra automatically creates an output directory - # Let's retrieve it and save some results there - save_path = HydraConfig.get().runtime.output_dir - - # Prepare function that will be used to spawn each client - client_fn = gen_client_fn( - fds, - tokenizer, - formatting_prompts_func, - data_collator, - cfg.model, - cfg.train, - save_path, - ) - - # Instantiate strategy according to config. Here we pass other arguments - # that are only defined at run time. - strategy = instantiate( - cfg.strategy, - on_fit_config_fn=get_on_fit_config(), - fit_metrics_aggregation_fn=fit_weighted_average, - evaluate_fn=get_evaluate_fn( - cfg.model, cfg.train.save_every_round, cfg.num_rounds, save_path - ), - ) - - # Start simulation - history = fl.simulation.start_simulation( - client_fn=client_fn, - num_clients=cfg.num_clients, - config=fl.server.ServerConfig(num_rounds=cfg.num_rounds), - client_resources={ - "num_cpus": cfg.client_resources.num_cpus, - "num_gpus": cfg.client_resources.num_gpus, - }, - strategy=strategy, - ) - - # Experiment completed. Now we save the results and - # generate plots using the `history` - print("................") - print(history) - - # Save results as a Python pickle using a file_path - # the directory created by Hydra for each run - with open(f"{save_path}/results.pkl", "wb") as f: - pickle.dump(history, f) - - -if __name__ == "__main__": - main() diff --git a/examples/llm-flowertune/requirements.txt b/examples/llm-flowertune/requirements.txt deleted file mode 100644 index 2d0e65da3615..000000000000 --- a/examples/llm-flowertune/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -flwr[rest,simulation]>=1.8.0, <2.0 -flwr-datasets>=0.0.2 -hydra-core==1.3.2 -trl==0.7.2 -bitsandbytes==0.41.3 -scipy==1.11.2 -peft==0.4.0 -fschat[model_worker,webui]==0.2.35 -transformers==4.38.1 -hf_transfer==0.1.8 diff --git a/examples/llm-flowertune/utils.py b/examples/llm-flowertune/utils.py deleted file mode 100644 index bbb607810537..000000000000 --- a/examples/llm-flowertune/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -from client import set_parameters -from models import get_model - - -# Get function that will be executed by the strategy's evaluate() method -# Here we use it to save global model checkpoints -def get_evaluate_fn(model_cfg, save_every_round, total_round, save_path): - """Return an evaluation function for saving global model.""" - - def evaluate(server_round: int, parameters, config): - # Save model - if server_round != 0 and ( - server_round == total_round or server_round % save_every_round == 0 - ): - # Init model - model = get_model(model_cfg) - set_parameters(model, parameters) - - model.save_pretrained(f"{save_path}/peft_{server_round}") - - return 0.0, {} - - return evaluate - - -# Get a function that will be used to construct the config that the client's -# fit() method will receive -def get_on_fit_config(): - def fit_config_fn(server_round: int): - fit_config = {"current_round": server_round} - return fit_config - - return fit_config_fn - - -def fit_weighted_average(metrics): - """Aggregation function for (federated) evaluation metrics.""" - # Multiply accuracy of each client by number of examples used - losses = [num_examples * m["train_loss"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - - # Aggregate and return custom metric (weighted average) - return {"train_loss": sum(losses) / sum(examples)} From 46374c8054c68eae6ba807820ffa9f055c4edb16 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Tue, 27 Aug 2024 18:49:17 +0100 Subject: [PATCH 138/188] fix(examples:skip) Fix redirect issue for ViT example (#4089) --- doc/source/conf.py | 1 - examples/doc/source/conf.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 5d434dd729bb..d3881325a5ce 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -195,7 +195,6 @@ def find_test_modules(package_path): "apiref-binaries": "ref-api-cli.html", "fedbn-example-pytorch-from-centralized-to-federated": "example-fedbn-pytorch-from-centralized-to-federated.html", "how-to-use-built-in-middleware-layers": "how-to-use-built-in-mods.html", - "vit-finetune": "flowertune-vit.html", # Restructuring: tutorials "tutorial/Flower-0-What-is-FL": "tutorial-series-what-is-federated-learning.html", "tutorial/Flower-1-Intro-to-FL-PyTorch": "tutorial-series-get-started-with-flower-pytorch.html", diff --git a/examples/doc/source/conf.py b/examples/doc/source/conf.py index 7712aa5f4f59..4e4b7b210051 100644 --- a/examples/doc/source/conf.py +++ b/examples/doc/source/conf.py @@ -67,6 +67,7 @@ "mxnet-from-centralized-to-federated": "index.html", "app-secure-aggregation": "flower-secure-aggregation.html", "llm-flowertune": "flowertune-llm.html", + "vit-finetune": "flowertune-vit.html", } From d02b81a14c5f211fbc3d7cae563f527a36b42974 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 28 Aug 2024 13:00:32 +0100 Subject: [PATCH 139/188] feat(framework) Add template to create a Flower Baseline (#3979) Co-authored-by: Taner Topal Co-authored-by: Daniel J. Beutel --- src/py/flwr/cli/new/new.py | 19 ++ src/py/flwr/cli/new/templates/app/LICENSE.tpl | 202 ++++++++++++++++++ .../new/templates/app/README.baseline.md.tpl | 127 +++++++++++ .../app/code/__init__.baseline.py.tpl | 1 + .../templates/app/code/client.baseline.py.tpl | 58 +++++ .../app/code/dataset.baseline.py.tpl | 36 ++++ .../templates/app/code/model.baseline.py.tpl | 80 +++++++ .../templates/app/code/server.baseline.py.tpl | 46 ++++ .../app/code/strategy.baseline.py.tpl | 1 + .../templates/app/code/utils.baseline.py.tpl | 1 + .../templates/app/pyproject.baseline.toml.tpl | 138 ++++++++++++ 11 files changed, 709 insertions(+) create mode 100644 src/py/flwr/cli/new/templates/app/LICENSE.tpl create mode 100644 src/py/flwr/cli/new/templates/app/README.baseline.md.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/__init__.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/client.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/dataset.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/model.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/server.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/strategy.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/code/utils.baseline.py.tpl create mode 100644 src/py/flwr/cli/new/templates/app/pyproject.baseline.toml.tpl diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 31da7b4ab9fb..9f2d32ddf99c 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -42,6 +42,7 @@ class MlFramework(str, Enum): MLX = "MLX" NUMPY = "NumPy" FLOWERTUNE = "FlowerTune" + BASELINE = "Flower Baseline" class LlmChallengeName(str, Enum): @@ -164,6 +165,8 @@ def new( llm_challenge_str = selected_value[0] llm_challenge_str = llm_challenge_str.lower() + is_baseline_project = framework_str == "baseline" + print( typer.style( f"\nπŸ”¨ Creating Flower App {app_name}...", @@ -193,6 +196,7 @@ def new( f"{import_name}/client_app.py": { "template": "app/code/flwr_tune/client_app.py.tpl" }, + f"{import_name}/app.py": {"template": "app/code/flwr_tune/app.py.tpl"}, f"{import_name}/models.py": { "template": "app/code/flwr_tune/models.py.tpl" }, @@ -255,6 +259,21 @@ def new( "template": f"app/code/task.{framework_str}.py.tpl" } + if is_baseline_project: + # Include additional files for baseline template + for file_name in ["model", "dataset", "strategy", "utils", "__init__"]: + files[f"{import_name}/{file_name}.py"] = { + "template": f"app/code/{file_name}.{framework_str}.py.tpl" + } + + # Replace README.md + files["README.md"]["template"] = f"app/README.{framework_str}.md.tpl" + + # Add LICENSE + files["LICENSE"] = {"template": "app/LICENSE.tpl"} + + context["framework_str"] = "baseline" + for file_path, value in files.items(): render_and_create( file_path=project_dir / file_path, diff --git a/src/py/flwr/cli/new/templates/app/LICENSE.tpl b/src/py/flwr/cli/new/templates/app/LICENSE.tpl new file mode 100644 index 000000000000..7a4a3ea2424c --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/LICENSE.tpl @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/py/flwr/cli/new/templates/app/README.baseline.md.tpl b/src/py/flwr/cli/new/templates/app/README.baseline.md.tpl new file mode 100644 index 000000000000..9bbbe8f22794 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/README.baseline.md.tpl @@ -0,0 +1,127 @@ +--- +title: title of the paper # TODO +url: https://arxiv.org/abs/2007.14390 # TODO: update with the link to your paper +labels: [label1, label2] # TODO: please add between 4 and 10 single-word (maybe two-words) labels (e.g. system heterogeneity, image classification, asynchronous, weight sharing, cross-silo). Do not use "". Remove this comment once you are done. +dataset: [dataset1, dataset2] # TODO: list of datasets you include in your baseline. Do not use "". Remove this comment once you are done. +--- + +> [!IMPORTANT] +> This is the template for your `README.md`. Please fill-in the information in all areas with a :warning: symbol. +> Please refer to the [Flower Baselines contribution](https://flower.ai/docs/baselines/how-to-contribute-baselines.html) and [Flower Baselines usage](https://flower.ai/docs/baselines/how-to-use-baselines.html) guides for more details. +> Please complete the metadata section at the very top of this README. This generates a table at the top of the file that will facilitate indexing baselines. +> Please remove this [!IMPORTANT] block once you are done with your `README.md` as well as all the `:warning:` symbols and the comments next to them. + +> [!IMPORTANT] +> To help having all baselines similarly formatted and structured, we have included two scripts in `baselines/dev` that when run will format your code and run some tests checking if it's formatted. +> These checks use standard packages such as `isort`, `black`, `pylint` and others. You as a baseline creator will need to install additional pacakges. These are already specified in the `pyproject.toml` of +> your baseline. Follow these steps: + +```bash +# Create a python env +pyenv virtualenv 3.10.14 $project_name + +# Activate it +pyenv activate $project_name + +# Install project including developer packages +# Note the `-e` this means you install it in editable mode +# so even if you change the code you don't need to do `pip install` +# again. However, if you add a new dependency to `pyproject.toml` you +# will need to re-run the command below +pip install -e ".[dev]" + +# Even without modifying or adding new code, you can run your baseline +# with the placeholder code generated when you did `flwr new`. If you +# want to test this to familiarise yourself with how flower apps are +# executed, execute this from the directory where you `pyproject.toml` is: +flwr run . + +# At anypoint during the process of creating your baseline you can +# run the formatting script. For this do: +cd .. # so you are in the `flower/baselines` directory + +# Run the formatting script (it will auto-correct issues if possible) +./dev/format-baseline.sh $project_name + +# Then, if the above is all good, run the tests. +./dev/test-baseline.sh $project_name +``` + +> [!IMPORTANT] +> When you open a PR to get the baseline merged into the main Flower repository, the `./dev/test-baseline.sh` script will run. Only if test pass, the baseline can be merged. +> Some issues highlighted by the tests script are easier than others to fix. Do not hesitate in reaching out for help to us (e.g. as a comment in your PR) if you are stuck with these. +> Before opening your PR, please remove the code snippet above as well all the [!IMPORTANT] message blocks. Yes, including this one. + +# :warning: *_Title of your baseline_* # Also copy this title to the `description` in the `[project]` section of your `pyproject.toml`. + +> [!NOTE] +> If you use this baseline in your work, please remember to cite the original authors of the paper as well as the Flower paper. + +**Paper:** :warning: *_add the URL of the paper page (not to the .pdf). For instance if you link a paper on ArXiv, add here the URL to the abstract page (e.g. [paper](https://arxiv.org/abs/1512.03385)). If your paper is in from a journal or conference proceedings, please follow the same logic._* + +**Authors:** :warning: *_list authors of the paper_* + +**Abstract:** :warning: *_add here the abstract of the paper you are implementing_* + + +## About this baseline + +**What’s implemented:** :warning: *_Concisely describe what experiment(s) (e.g. Figure 1, Table 2, etc) in the publication can be replicated by running the code. Please only use a few sentences. ”_* + +**Datasets:** :warning: *_List the datasets you used (if you used a medium to large dataset, >10GB please also include the sizes of the dataset). We highly recommend using [FlowerDatasets](https://flower.ai/docs/datasets/index.html) to download and partition your dataset. If you have other ways to download the data, you can also use `FlowerDatasets` to partiion it._* + +**Hardware Setup:** :warning: *_Give some details about the hardware (e.g. a server with 8x V100 32GB and 256GB of RAM) you used to run the experiments for this baseline. Indicate how long it took to run the experiments. Someone out there might not have access to the same resources you have so, could you list the absolute minimum hardware needed to run the experiment in a reasonable amount of time ? (e.g. minimum is 1x 16GB GPU otherwise a client model can’t be trained with a sufficiently large batch size). Could you test this works too?_* + +**Contributors:** :warning: *_let the world know who contributed to this baseline. This could be either your name, your name and affiliation at the time, or your GitHub profile name if you prefer. If multiple contributors signed up for this baseline, please list yourself and your colleagues_* + + +## Experimental Setup + +**Task:** :warning: *_what’s the primary task that is being federated? (e.g. image classification, next-word prediction). If you have experiments for several, please list them_* + +**Model:** :warning: *_provide details about the model you used in your experiments (if more than use a list). If your model is small, describing it as a table would be :100:. Some FL methods do not use an off-the-shelve model (e.g. ResNet18) instead they create your own. If this is your case, please provide a summary here and give pointers to where in the paper (e.g. Appendix B.4) is detailed._* + +**Dataset:** :warning: *_Earlier you listed already the datasets that your baseline uses. Now you should include a breakdown of the details about each of them. Please include information about: how the dataset is partitioned (e.g. LDA with alpha 0.1 as default and all clients have the same number of training examples; or each client gets assigned a different number of samples following a power-law distribution with each client only instances of 2 classes)? if your dataset is naturally partitioned just state β€œnaturally partitioned”; how many partitions there are (i.e. how many clients)? Please include this an all information relevant about the dataset and its partitioning into a table._* + +**Training Hyperparameters:** :warning: *_Include a table with all the main hyperparameters in your baseline. Please show them with their default value._* + + +## Environment Setup + +:warning: _Specify the steps to create and activate your environment and install the baseline project. Most baselines are expected to require minimal steps as shown below. These instructions should be comprehensive enough so anyone can run them (if non standard, describe them step-by-step)._ + +:warning: _The dependencies for your baseline are listed in the `pyproject.toml`, extend it with additional packages needed for your baseline._ + +:warning: _Baselines should use Python 3.10, [pyenv](https://github.com/pyenv/pyenv), and the [virtualenv](https://github.com/pyenv/pyenv-virtualenv) plugging. + +```bash +# Create the virtual environment +pyenv virtualenv 3.10.14 + +# Activate it +pyenv activate + +# Install the baseline +pip install -e . +``` + +:warning: _If your baseline requires running some script before starting an experiment, please indicate so here_. + +## Running the Experiments + +:warning: _Make sure you have adjusted the `client-resources` in the federation in `pyproject.toml` so your simulation makes the best use of the system resources available._ + +:warning: _Your baseline implementation should replicate several of the experiments in the original paper. Please include here the exact command(s) needed to run each of those experiments followed by a figure (e.g. a line plot) or table showing the results you obtained when you ran the code. Below is an example of how you can present this. Please add command followed by results for all your experiments._ + +:warning: _You might want to add more hyperparameters and settings for your baseline. You can do so by extending `[tool.flwr.app.config]` in `pyproject.toml`. In addition, you can create a new `.toml` file that can be passed with the `--run-config` command (see below an example) to override several config values **already present** in `pyproject.toml`._ +```bash +# it is likely that for one experiment you need to override some arguments. +flwr run . --run-config learning-rate=0.1,coefficient=0.123 + +# or you might want to load different `.toml` configs all together: +flwr run . --run-config .toml +``` + +:warning: _It is preferable to show a single commmand (or multilple commands if they belong to the same experiment) and then a table/plot with the expected results, instead of showing all the commands first and then all the results/plots._ +:warning: _If you present plots or other figures, please include either a Jupyter notebook showing how to create them or include a utility function that can be called after the experiments finish running._ +:warning: If you include plots or figures, save them in `.png` format and place them in a new directory named `_static` at the same level as your `README.md`. diff --git a/src/py/flwr/cli/new/templates/app/code/__init__.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/__init__.baseline.py.tpl new file mode 100644 index 000000000000..5ad8041381d6 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/__init__.baseline.py.tpl @@ -0,0 +1 @@ +"""$project_name: A Flower Baseline.""" diff --git a/src/py/flwr/cli/new/templates/app/code/client.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.baseline.py.tpl new file mode 100644 index 000000000000..83a475f20d27 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/client.baseline.py.tpl @@ -0,0 +1,58 @@ +"""$project_name: A Flower Baseline.""" + +import torch + +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context +from $import_name.dataset import load_data +from $import_name.model import Net, get_weights, set_weights, test, train + + +class FlowerClient(NumPyClient): + """A class defining the client.""" + + def __init__(self, net, trainloader, valloader, local_epochs): + self.net = net + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.net.to(self.device) + + def fit(self, parameters, config): + """Traim model using this client's data.""" + set_weights(self.net, parameters) + train_loss = train( + self.net, + self.trainloader, + self.local_epochs, + self.device, + ) + return ( + get_weights(self.net), + len(self.trainloader.dataset), + {"train_loss": train_loss}, + ) + + def evaluate(self, parameters, config): + """Evaluate model using this client's data.""" + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + # Load model and data + net = Net() + partition_id = int(context.node_config["partition-id"]) + num_partitions = int(context.node_config["num-partitions"]) + trainloader, valloader = load_data(partition_id, num_partitions) + local_epochs = context.run_config["local-epochs"] + + # Return Client instance + return FlowerClient(net, trainloader, valloader, local_epochs).to_client() + + +# Flower ClientApp +app = ClientApp(client_fn) diff --git a/src/py/flwr/cli/new/templates/app/code/dataset.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/dataset.baseline.py.tpl new file mode 100644 index 000000000000..46f1f64418c0 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/dataset.baseline.py.tpl @@ -0,0 +1,36 @@ +"""$project_name: A Flower Baseline.""" + +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor + +FDS = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + """Load partition CIFAR10 data.""" + # Only initialize `FederatedDataset` once + global FDS # pylint: disable=global-statement + if FDS is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + FDS = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = FDS.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + return trainloader, testloader diff --git a/src/py/flwr/cli/new/templates/app/code/model.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/model.baseline.py.tpl new file mode 100644 index 000000000000..8a914fcf60d1 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/model.baseline.py.tpl @@ -0,0 +1,80 @@ +"""$project_name: A Flower Baseline.""" + +from collections import OrderedDict + +import torch +import torch.nn.functional as F +from torch import nn + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz').""" + + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + """Do forward.""" + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def train(net, trainloader, epochs, device): + """Train the model on the training set.""" + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss() + criterion.to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9) + net.train() + running_loss = 0.0 + for _ in range(epochs): + for batch in trainloader: + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + loss = criterion(net(images.to(device)), labels.to(device)) + loss.backward() + optimizer.step() + running_loss += loss.item() + + avg_trainloss = running_loss / len(trainloader) + return avg_trainloss + + +def test(net, testloader, device): + """Validate the model on the test set.""" + net.to(device) + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + loss = loss / len(testloader) + return loss, accuracy + + +def get_weights(net): + """Extract model parameters as numpy arrays from state_dict.""" + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + """Apply parameters to an existing model.""" + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) diff --git a/src/py/flwr/cli/new/templates/app/code/server.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/server.baseline.py.tpl new file mode 100644 index 000000000000..ea536e3efffb --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/server.baseline.py.tpl @@ -0,0 +1,46 @@ +"""$project_name: A Flower Baseline.""" + +from typing import List, Tuple + +from flwr.common import Context, Metrics, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedAvg +from $import_name.model import Net, get_weights + + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + """Do weighted average of accuracy metric.""" + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * float(m["accuracy"]) for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + """Construct components that set the ServerApp behaviour.""" + # Read from config + num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Define strategy + strategy = FedAvg( + fraction_fit=float(fraction_fit), + fraction_evaluate=1.0, + min_available_clients=2, + initial_parameters=parameters, + evaluate_metrics_aggregation_fn=weighted_average, + ) + config = ServerConfig(num_rounds=int(num_rounds)) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/src/py/flwr/cli/new/templates/app/code/strategy.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/strategy.baseline.py.tpl new file mode 100644 index 000000000000..5ad8041381d6 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/strategy.baseline.py.tpl @@ -0,0 +1 @@ +"""$project_name: A Flower Baseline.""" diff --git a/src/py/flwr/cli/new/templates/app/code/utils.baseline.py.tpl b/src/py/flwr/cli/new/templates/app/code/utils.baseline.py.tpl new file mode 100644 index 000000000000..5ad8041381d6 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/code/utils.baseline.py.tpl @@ -0,0 +1 @@ +"""$project_name: A Flower Baseline.""" diff --git a/src/py/flwr/cli/new/templates/app/pyproject.baseline.toml.tpl b/src/py/flwr/cli/new/templates/app/pyproject.baseline.toml.tpl new file mode 100644 index 000000000000..71afc184ffa9 --- /dev/null +++ b/src/py/flwr/cli/new/templates/app/pyproject.baseline.toml.tpl @@ -0,0 +1,138 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "$package_name" +version = "1.0.0" +description = "" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.11.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[project.optional-dependencies] +dev = [ + "isort==5.13.2", + "black==24.2.0", + "docformatter==1.7.5", + "mypy==1.8.0", + "pylint==3.2.6", + "flake8==5.0.4", + "pytest==6.2.4", + "pytest-watch==4.2.0", + "ruff==0.1.9", + "types-requests==2.31.0.20240125", +] + +[tool.isort] +profile = "black" +known_first_party = ["flwr"] + +[tool.black] +line-length = 88 +target-version = ["py38", "py39", "py310", "py311"] + +[tool.pytest.ini_options] +minversion = "6.2" +addopts = "-qq" +testpaths = [ + "flwr_baselines", +] + +[tool.mypy] +ignore_missing_imports = true +strict = false +plugins = "numpy.typing.mypy_plugin" + +[tool.pylint."MESSAGES CONTROL"] +disable = "duplicate-code,too-few-public-methods,useless-import-alias" +good-names = "i,j,k,_,x,y,X,Y,K,N" +max-args = 10 +max-attributes = 15 +max-locals = 36 +max-branches = 20 +max-statements = 55 + +[tool.pylint.typecheck] +generated-members = "numpy.*, torch.*, tensorflow.*" + +[[tool.mypy.overrides]] +module = [ + "importlib.metadata.*", + "importlib_metadata.*", +] +follow_imports = "skip" +follow_imports_for_stubs = true +disallow_untyped_calls = false + +[[tool.mypy.overrides]] +module = "torch.*" +follow_imports = "skip" +follow_imports_for_stubs = true + +[tool.docformatter] +wrap-summaries = 88 +wrap-descriptions = 88 + +[tool.ruff] +target-version = "py38" +line-length = 88 +select = ["D", "E", "F", "W", "B", "ISC", "C4"] +fixable = ["D", "E", "F", "W", "B", "ISC", "C4"] +ignore = ["B024", "B027"] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "proto", +] + +[tool.ruff.pydocstyle] +convention = "numpy" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "$username" + +[tool.flwr.app.components] +serverapp = "$import_name.server_app:app" +clientapp = "$import_name.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +fraction-fit = 0.5 +local-epochs = 1 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 10 +options.backend.client-resources.num-cpus = 2 +options.backend.client-resources.num-gpus = 0.0 From 2dd161ac4bf3e53d020509c1e3c9c5bd3476b0d1 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 28 Aug 2024 18:39:58 +0200 Subject: [PATCH 140/188] fix(framework:skip) Parse node config if set (#4091) --- src/py/flwr/client/supernode/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 8d28e69dea6e..ac845417415e 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -77,7 +77,9 @@ def run_supernode() -> None: authentication_keys=authentication_keys, max_retries=args.max_retries, max_wait_time=args.max_wait_time, - node_config=parse_config_args([args.node_config]), + node_config=parse_config_args( + [args.node_config] if args.node_config else args.node_config + ), isolation=args.isolation, supernode_address=args.supernode_address, ) From 7ae277075009458c1e4a9d11268b77bdad675945 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 28 Aug 2024 18:52:16 +0200 Subject: [PATCH 141/188] fix(framework:skip) Support overriding run config from a `TOML` (#4080) --- src/py/flwr/common/config.py | 25 +++++++++------- src/py/flwr/common/config_test.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index eec7cfb726b7..42039fa959ac 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -185,23 +185,26 @@ def parse_config_args( if config is None: return overrides + # Handle if .toml file is passed + if len(config) == 1 and config[0].endswith(".toml"): + with Path(config[0]).open("rb") as config_file: + overrides = flatten_dict(tomli.load(config_file)) + return overrides + # Regular expression to capture key-value pairs with possible quoted values pattern = re.compile(r"(\S+?)=(\'[^\']*\'|\"[^\"]*\"|\S+)") for config_line in config: if config_line: - matches = pattern.findall(config_line) + # .toml files aren't allowed alongside other configs + if config_line.endswith(".toml"): + raise ValueError( + "TOML files cannot be passed alongside key-value pairs." + ) - if ( - len(matches) == 1 - and "=" not in matches[0][0] - and matches[0][0].endswith(".toml") - ): - with Path(matches[0][0]).open("rb") as config_file: - overrides = flatten_dict(tomli.load(config_file)) - else: - toml_str = "\n".join(f"{k} = {v}" for k, v in matches) - overrides.update(tomli.loads(toml_str)) + matches = pattern.findall(config_line) + toml_str = "\n".join(f"{k} = {v}" for k, v in matches) + overrides.update(tomli.loads(toml_str)) return overrides diff --git a/src/py/flwr/common/config_test.py b/src/py/flwr/common/config_test.py index 712e07264d3f..34bc691cc957 100644 --- a/src/py/flwr/common/config_test.py +++ b/src/py/flwr/common/config_test.py @@ -15,6 +15,7 @@ """Test util functions handling Flower config.""" import os +import tempfile import textwrap from pathlib import Path from unittest.mock import patch @@ -254,3 +255,50 @@ def test_parse_config_args_overrides() -> None: "key5": True, "key6": "value6", } + + +def test_parse_config_args_from_toml_file() -> None: + """Test if a toml passed to --run-config it is loaded and fused correctly.""" + # Will be saved as a temp .toml file + toml_config = """ + num-server-rounds = 10 + momentum = 0.1 + verbose = true + """ + # This is the UserConfig that would be extracted from pyproject.toml + initial_run_config: UserConfig = { + "num-server-rounds": 5, + "momentum": 0.2, + "dataset": "my-fancy-dataset", + "verbose": False, + } + expected_config = { + "num-server-rounds": 10, + "momentum": 0.1, + "dataset": "my-fancy-dataset", + "verbose": True, + } + + # Create a temporary directory using a context manager + with tempfile.TemporaryDirectory() as temp_dir: + # Create a temporary TOML file within that directory + toml_config_file = os.path.join(temp_dir, "extra_config.toml") + + # Write the data to the TOML file + with open(toml_config_file, "w", encoding="utf-8") as toml_file: + toml_file.write(textwrap.dedent(toml_config)) + + # Parse config (this mimics what `--run-config path/to/config.toml` does) + config_from_toml = parse_config_args([toml_config_file]) + # Fuse + config = fuse_dicts(initial_run_config, config_from_toml) + + # Assert + assert config == expected_config + + +def test_parse_config_args_passing_toml_and_key_value() -> None: + """Test that passing a toml and key-value configs aren't allowed.""" + config = ["my-other-config.toml", "lr=0.1", "epochs=99"] + with pytest.raises(ValueError): + parse_config_args(config) From 84eeda695f7eadd10bb8a1ee5d69c52a098a9393 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 29 Aug 2024 10:46:55 +0200 Subject: [PATCH 142/188] fix(framework:skip) Fix parsing run config in simulation (#4095) --- src/py/flwr/simulation/run_simulation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index af12da4a5814..1eddd91108d8 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -177,7 +177,9 @@ def run_simulation_from_cli() -> None: client_app_attr = app_components["clientapp"] server_app_attr = app_components["serverapp"] - override_config = parse_config_args([args.run_config]) + override_config = parse_config_args( + [args.run_config] if args.run_config else args.run_config + ) fused_config = get_fused_config_from_dir(app_path, override_config) app_dir = args.app is_app = True From e3d74f7e7e8bd505ece0e12a17e4321b1a80863d Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 29 Aug 2024 13:20:41 +0200 Subject: [PATCH 143/188] refactor(baselines:skip) Remove baseline template and creation script (#4082) --- .../baseline_template/EXTENDED_README.md | 123 ----------- baselines/baseline_template/LICENSE | 202 ------------------ baselines/baseline_template/README.md | 87 -------- .../baseline_template/__init__.py | 1 - .../baseline_template/client.py | 5 - .../baseline_template/conf/base.yaml | 17 -- .../baseline_template/dataset.py | 10 - .../baseline_template/dataset_preparation.py | 34 --- .../baseline_template/main.py | 57 ----- .../baseline_template/models.py | 7 - .../baseline_template/server.py | 5 - .../baseline_template/strategy.py | 5 - .../baseline_template/utils.py | 6 - baselines/baseline_template/pyproject.toml | 137 ------------ baselines/dev/create-baseline.sh | 30 --- 15 files changed, 726 deletions(-) delete mode 100644 baselines/baseline_template/EXTENDED_README.md delete mode 100644 baselines/baseline_template/LICENSE delete mode 100644 baselines/baseline_template/README.md delete mode 100644 baselines/baseline_template/baseline_template/__init__.py delete mode 100644 baselines/baseline_template/baseline_template/client.py delete mode 100644 baselines/baseline_template/baseline_template/conf/base.yaml delete mode 100644 baselines/baseline_template/baseline_template/dataset.py delete mode 100644 baselines/baseline_template/baseline_template/dataset_preparation.py delete mode 100644 baselines/baseline_template/baseline_template/main.py delete mode 100644 baselines/baseline_template/baseline_template/models.py delete mode 100644 baselines/baseline_template/baseline_template/server.py delete mode 100644 baselines/baseline_template/baseline_template/strategy.py delete mode 100644 baselines/baseline_template/baseline_template/utils.py delete mode 100644 baselines/baseline_template/pyproject.toml delete mode 100755 baselines/dev/create-baseline.sh diff --git a/baselines/baseline_template/EXTENDED_README.md b/baselines/baseline_template/EXTENDED_README.md deleted file mode 100644 index 9c8f5bc72fa9..000000000000 --- a/baselines/baseline_template/EXTENDED_README.md +++ /dev/null @@ -1,123 +0,0 @@ - -# Extended Readme - -> The baselines are expected to run in a machine running Ubuntu 22.04 - -While `README.md` should include information about the baseline you implement and how to run it, this _extended_ readme provides info on what's the expected directory structure for a new baseline and more generally the instructions to follow before your baseline can be merged into the Flower repository. Please follow closely these instructions. It is likely that you have already completed steps 1-2. - -1. Fork the Flower repository and clone it. -2. Navigate to the `baselines/` directory and from there run: - ```bash - # This will create a new directory with the same structure as this `baseline_template` directory. - ./dev/create-baseline.sh - ``` -3. All your code and configs should go into a sub-directory with the same name as the name of your baseline. - * The sub-directory contains a series of Python scripts that you can edit. Please stick to these files and consult with us if you need additional ones. - * There is also a basic config structure in `/conf` ready be parsed by [Hydra](https://hydra.cc/) when executing your `main.py`. -4. Therefore, the directory structure in your baseline should look like: - ```bash - baselines/ - β”œβ”€β”€ README.md # describes your baseline and everything needed to use it - β”œβ”€β”€ EXTENDED_README.md # to remove before creating your PR - β”œβ”€β”€ pyproject.toml # details your Python environment - └── - β”œβ”€β”€ *.py # several .py files including main.py and __init__.py - └── conf - └── *.yaml # one or more Hydra config files - - ``` -> :warning: Make sure the variable `name` in `pyproject.toml` is set to the name of the sub-directory containing all your code. - -5. Add your dependencies to the `pyproject.toml` (see below a few examples on how to do it). Read more about Poetry below in this `EXTENDED_README.md`. -6. Regularly check that your coding style and the documentation you add follow good coding practices. To test whether your code meets the requirements, please run the following: - ```bash - # After activating your environment and from your baseline's directory - cd .. # to go to the top-level directory of all baselines - ./dev/test-baseline.sh - ./dev/test-baseline-structure.sh - ``` - Both `test-baseline.sh` and `test-baseline-structure.sh` will also be automatically run when you create a PR, and both tests need to pass for the baseline to be merged. - To automatically solve some formatting issues and apply easy fixes, please run the formatting script: - ```bash - # After activating your environment and from your baseline's directory - cd .. # to go to the top-level directory of all baselines - ./dev/format-baseline.sh - ``` -7. Ensure that the Python environment for your baseline can be created without errors by simply running `poetry install` and that this is properly described later when you complete the `Environment Setup` section in `README.md`. This is specially important if your environment requires additional steps after doing `poetry install`. -8. Ensure that your baseline runs with default arguments by running `poetry run python -m .main`. Then, describe this and other forms of running your code in the `Running the Experiments` section in `README.md`. -9. Once your code is ready and you have checked: - * that following the instructions in your `README.md` the Python environment can be created correctly - - * that running the code following your instructions can reproduce the experiments in the paper - - , then you just need to create a Pull Request (PR) to kickstart the process of merging your baseline into the Flower repository. - -> Once you are happy to merge your baseline contribution, please delete this `EXTENDED_README.md` file. - - -## About Poetry - -We use Poetry to manage the Python environment for each individual baseline. You can follow the instructions [here](https://python-poetry.org/docs/) to install Poetry in your machine. - - -### Specifying a Python Version (optional) -By default, Poetry will use the Python version in your system. In some settings, you might want to specify a particular version of Python to use inside your Poetry environment. You can do so with [`pyenv`](https://github.com/pyenv/pyenv). Check the documentation for the different ways of installing `pyenv`, but one easy way is using the [automatic installer](https://github.com/pyenv/pyenv-installer): -```bash -curl https://pyenv.run | bash # then, don't forget links to your .bashrc/.zshrc -``` - -You can then install any Python version with `pyenv install ` (e.g. `pyenv install 3.9.17`). Then, in order to use that version for your baseline, you'd do the following: - -```bash -# cd to your baseline directory (i.e. where the `pyproject.toml` is) -pyenv local - -# set that version for poetry -poetry env use - -# then you can install your Poetry environment (see the next setp) -``` - -### Installing Your Environment -With the Poetry tool already installed, you can create an environment for this baseline with commands: -```bash -# run this from the same directory as the `pyproject.toml` file is -poetry install -``` - -This will create a basic Python environment with just Flower and additional packages, including those needed for simulation. Next, you should add the dependencies for your code. It is **critical** that you fix the version of the packages you use using a `=` not a `=^`. You can do so via [`poetry add`](https://python-poetry.org/docs/cli/#add). Below are some examples: - -```bash -# For instance, if you want to install tqdm -poetry add tqdm==4.65.0 - -# If you already have a requirements.txt, you can add all those packages (but ensure you have fixed the version) in one go as follows: -poetry add $( cat requirements.txt ) -``` -With each `poetry add` command, the `pyproject.toml` gets automatically updated so you don't need to keep that `requirements.txt` as part of this baseline. - - -More critically however, is adding your ML framework of choice to the list of dependencies. For some frameworks you might be able to do so with the `poetry add` command. Check [the Poetry documentation](https://python-poetry.org/docs/cli/#add) for how to add packages in various ways. For instance, let's say you want to use PyTorch: - -```bash -# with plain `pip` you'd run a command such as: -pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu117 - -# to add the same 3 dependencies to your Poetry environment you'd need to add the URL to the wheel that the above pip command auto-resolves for you. -# You can find those wheels in `https://download.pytorch.org/whl/cu117`. Copy the link and paste it after the `poetry add` command. -# For instance to add `torch==1.13.1+cu117` and a x86 Linux system with Python3.8 you'd: -poetry add https://download.pytorch.org/whl/cu117/torch-1.13.1%2Bcu117-cp38-cp38-linux_x86_64.whl -# you'll need to repeat this for both `torchvision` and `torchaudio` -``` -The above is just an example of how you can add these dependencies. Please refer to the Poetry documentation to extra reference. - -If all attempts fail, you can still install packages via standard `pip`. You'd first need to source/activate your Poetry environment. -```bash -# first ensure you have created your environment -# and installed the base packages provided in the template -poetry install - -# then activate it -poetry shell -``` -Now you are inside your environment (pretty much as when you use `virtualenv` or `conda`) so you can install further packages with `pip`. Please note that, unlike with `poetry add`, these extra requirements won't be captured by `pyproject.toml`. Therefore, please ensure that you provide all instructions needed to: (1) create the base environment with Poetry and (2) install any additional dependencies via `pip` when you complete your `README.md`. \ No newline at end of file diff --git a/baselines/baseline_template/LICENSE b/baselines/baseline_template/LICENSE deleted file mode 100644 index d64569567334..000000000000 --- a/baselines/baseline_template/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/baselines/baseline_template/README.md b/baselines/baseline_template/README.md deleted file mode 100644 index ee6e1e96976f..000000000000 --- a/baselines/baseline_template/README.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: title of the paper -url: URL to the paper page (not the pdf) -labels: [label1, label2] # please add between 4 and 10 single-word (maybe two-words) labels (e.g. system heterogeneity, image classification, asynchronous, weight sharing, cross-silo). Do not use "" -dataset: [dataset1, dataset2] # list of datasets you include in your baseline. Do not use "" ---- - -# :warning: *_Title of your baseline_* - -> Note: If you use this baseline in your work, please remember to cite the original authors of the paper as well as the Flower paper. - -> :warning: This is the template to follow when creating a new Flower Baseline. Please follow the instructions in `EXTENDED_README.md` - -> :warning: Please follow the instructions carefully. You can see the [FedProx-MNIST baseline](https://github.com/adap/flower/tree/main/baselines/fedprox) as an example of a baseline that followed this guide. - -> :warning: Please complete the metadata section at the very top of this README. This generates a table at the top of the file that will facilitate indexing baselines. - -**Paper:** :warning: *_add the URL of the paper page (not to the .pdf). For instance if you link a paper on ArXiv, add here the URL to the abstract page (e.g. https://arxiv.org/abs/1512.03385). If your paper is in from a journal or conference proceedings, please follow the same logic._* - -**Authors:** :warning: *_list authors of the paper_* - -**Abstract:** :warning: *_add here the abstract of the paper you are implementing_* - - -## About this baseline - -**What’s implemented:** :warning: *_Concisely describe what experiment(s) in the publication can be replicated by running the code. Please only use a few sentences. Start with: β€œThe code in this directory …”_* - -**Datasets:** :warning: *_List the datasets you used (if you used a medium to large dataset, >10GB please also include the sizes of the dataset)._* - -**Hardware Setup:** :warning: *_Give some details about the hardware (e.g. a server with 8x V100 32GB and 256GB of RAM) you used to run the experiments for this baseline. Someone out there might not have access to the same resources you have so, could list the absolute minimum hardware needed to run the experiment in a reasonable amount of time ? (e.g. minimum is 1x 16GB GPU otherwise a client model can’t be trained with a sufficiently large batch size). Could you test this works too?_* - -**Contributors:** :warning: *_let the world know who contributed to this baseline. This could be either your name, your name and affiliation at the time, or your GitHub profile name if you prefer. If multiple contributors signed up for this baseline, please list yourself and your colleagues_* - - -## Experimental Setup - -**Task:** :warning: *_what’s the primary task that is being federated? (e.g. image classification, next-word prediction). If you have experiments for several, please list them_* - -**Model:** :warning: *_provide details about the model you used in your experiments (if more than use a list). If your model is small, describing it as a table would be :100:. Some FL methods do not use an off-the-shelve model (e.g. ResNet18) instead they create your own. If this is your case, please provide a summary here and give pointers to where in the paper (e.g. Appendix B.4) is detailed._* - -**Dataset:** :warning: *_Earlier you listed already the datasets that your baseline uses. Now you should include a breakdown of the details about each of them. Please include information about: how the dataset is partitioned (e.g. LDA with alpha 0.1 as default and all clients have the same number of training examples; or each client gets assigned a different number of samples following a power-law distribution with each client only instances of 2 classes)? if your dataset is naturally partitioned just state β€œnaturally partitioned”; how many partitions there are (i.e. how many clients)? Please include this an all information relevant about the dataset and its partitioning into a table._* - -**Training Hyperparameters:** :warning: *_Include a table with all the main hyperparameters in your baseline. Please show them with their default value._* - - -## Environment Setup - -:warning: _The Python environment for all baselines should follow these guidelines in the `EXTENDED_README`. Specify the steps to create and activate your environment. If there are any external system-wide requirements, please include instructions for them too. These instructions should be comprehensive enough so anyone can run them (if non standard, describe them step-by-step)._ - - -## Running the Experiments - -:warning: _Provide instructions on the steps to follow to run all the experiments._ -```bash -# The main experiment implemented in your baseline using default hyperparameters (that should be setup in the Hydra configs) should run (including dataset download and necessary partitioning) by executing the command: - -poetry run python -m .main # where is the name of this directory and that of the only sub-directory in this directory (i.e. where all your source code is) - -# If you are using a dataset that requires a complicated download (i.e. not using one natively supported by TF/PyTorch) + preprocessing logic, you might want to tell people to run one script first that will do all that. Please ensure the download + preprocessing can be configured to suit (at least!) a different download directory (and use as default the current directory). The expected command to run to do this is: - -poetry run python -m .dataset_preparation - -# It is expected that you baseline supports more than one dataset and different FL settings (e.g. different number of clients, dataset partitioning methods, etc). Please provide a list of commands showing how these experiments are run. Include also a short explanation of what each one does. Here it is expected you'll be using the Hydra syntax to override the default config. - -poetry run python -m .main -. -. -. -poetry run python -m .main -``` - - -## Expected Results - -:warning: _Your baseline implementation should replicate several of the experiments in the original paper. Please include here the exact command(s) needed to run each of those experiments followed by a figure (e.g. a line plot) or table showing the results you obtained when you ran the code. Below is an example of how you can present this. Please add command followed by results for all your experiments._ - -```bash -# it is likely that for one experiment you need to sweep over different hyperparameters. You are encouraged to use Hydra's multirun functionality for this. This is an example of how you could achieve this for some typical FL hyperparameteres - -poetry run python -m .main --multirun num_client_per_round=5,10,50 dataset=femnist,cifar10 -# the above command will run a total of 6 individual experiments (because 3client_configs x 2datasets = 6 -- you can think of it as a grid). - -[Now show a figure/table displaying the results of the above command] - -# add more commands + plots for additional experiments. -``` diff --git a/baselines/baseline_template/baseline_template/__init__.py b/baselines/baseline_template/baseline_template/__init__.py deleted file mode 100644 index a5e567b59135..000000000000 --- a/baselines/baseline_template/baseline_template/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Template baseline package.""" diff --git a/baselines/baseline_template/baseline_template/client.py b/baselines/baseline_template/baseline_template/client.py deleted file mode 100644 index d2e2206111f3..000000000000 --- a/baselines/baseline_template/baseline_template/client.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Define your client class and a function to construct such clients. - -Please overwrite `flwr.client.NumPyClient` or `flwr.client.Client` and create a function -to instantiate your client. -""" diff --git a/baselines/baseline_template/baseline_template/conf/base.yaml b/baselines/baseline_template/baseline_template/conf/base.yaml deleted file mode 100644 index 2d65b3b989b2..000000000000 --- a/baselines/baseline_template/baseline_template/conf/base.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -# this is the config that will be loaded as default by main.py -# Please follow the provided structure (this will ensuring all baseline follow -# a similar configuration structure and hence be easy to customise) - -dataset: - # dataset config - -model: - # model config - -strategy: - _target_: # points to your strategy (either custom or exiting in Flower) - # rest of strategy config - -client: - # client config diff --git a/baselines/baseline_template/baseline_template/dataset.py b/baselines/baseline_template/baseline_template/dataset.py deleted file mode 100644 index 5e436abe12fb..000000000000 --- a/baselines/baseline_template/baseline_template/dataset.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Handle basic dataset creation. - -In case of PyTorch it should return dataloaders for your dataset (for both the clients -and the server). If you are using a custom dataset class, this module is the place to -define it. If your dataset requires to be downloaded (and this is not done -automatically -- e.g. as it is the case for many dataset in TorchVision) and -partitioned, please include all those functions and logic in the -`dataset_preparation.py` module. You can use all those functions from functions/methods -defined here of course. -""" diff --git a/baselines/baseline_template/baseline_template/dataset_preparation.py b/baselines/baseline_template/baseline_template/dataset_preparation.py deleted file mode 100644 index bd3440b9276b..000000000000 --- a/baselines/baseline_template/baseline_template/dataset_preparation.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Handle the dataset partitioning and (optionally) complex downloads. - -Please add here all the necessary logic to either download, uncompress, pre/post-process -your dataset (or all of the above). If the desired way of running your baseline is to -first download the dataset and partition it and then run the experiments, please -uncomment the lines below and tell us in the README.md (see the "Running the Experiment" -block) that this file should be executed first. -""" -# import hydra -# from hydra.core.hydra_config import HydraConfig -# from hydra.utils import call, instantiate -# from omegaconf import DictConfig, OmegaConf - - -# @hydra.main(config_path="conf", config_name="base", version_base=None) -# def download_and_preprocess(cfg: DictConfig) -> None: -# """Does everything needed to get the dataset. - -# Parameters -# ---------- -# cfg : DictConfig -# An omegaconf object that stores the hydra config. -# """ - -# ## 1. print parsed config -# print(OmegaConf.to_yaml(cfg)) - -# # Please include here all the logic -# # Please use the Hydra config style as much as possible specially -# # for parts that can be customised (e.g. how data is partitioned) - -# if __name__ == "__main__": - -# download_and_preprocess() diff --git a/baselines/baseline_template/baseline_template/main.py b/baselines/baseline_template/baseline_template/main.py deleted file mode 100644 index 25ae1bec6a10..000000000000 --- a/baselines/baseline_template/baseline_template/main.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Create and connect the building blocks for your experiments; start the simulation. - -It includes processioning the dataset, instantiate strategy, specify how the global -model is going to be evaluated, etc. At the end, this script saves the results. -""" -# these are the basic packages you'll need here -# feel free to remove some if aren't needed -import hydra -from omegaconf import DictConfig, OmegaConf - - -@hydra.main(config_path="conf", config_name="base", version_base=None) -def main(cfg: DictConfig) -> None: - """Run the baseline. - - Parameters - ---------- - cfg : DictConfig - An omegaconf object that stores the hydra config. - """ - # 1. Print parsed config - print(OmegaConf.to_yaml(cfg)) - - # 2. Prepare your dataset - # here you should call a function in datasets.py that returns whatever is needed to: - # (1) ensure the server can access the dataset used to evaluate your model after - # aggregation - # (2) tell each client what dataset partitions they should use (e.g. a this could - # be a location in the file system, a list of dataloader, a list of ids to extract - # from a dataset, it's up to you) - - # 3. Define your clients - # Define a function that returns another function that will be used during - # simulation to instantiate each individual client - # client_fn = client.() - - # 4. Define your strategy - # pass all relevant argument (including the global dataset used after aggregation, - # if needed by your method.) - # strategy = instantiate(cfg.strategy, ) - - # 5. Start Simulation - # history = fl.simulation.start_simulation() - - # 6. Save your results - # Here you can save the `history` returned by the simulation and include - # also other buffers, statistics, info needed to be saved in order to later - # on generate the plots you provide in the README.md. You can for instance - # access elements that belong to the strategy for example: - # data = strategy.get_my_custom_data() -- assuming you have such method defined. - # Hydra will generate for you a directory each time you run the code. You - # can retrieve the path to that directory with this: - # save_path = HydraConfig.get().runtime.output_dir - - -if __name__ == "__main__": - main() diff --git a/baselines/baseline_template/baseline_template/models.py b/baselines/baseline_template/baseline_template/models.py deleted file mode 100644 index 71fa553d1f59..000000000000 --- a/baselines/baseline_template/baseline_template/models.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Define our models, and training and eval functions. - -If your model is 100% off-the-shelf (e.g. directly from torchvision without requiring -modifications) you might be better off instantiating your model directly from the Hydra -config. In this way, swapping your model for another one can be done without changing -the python code at all -""" diff --git a/baselines/baseline_template/baseline_template/server.py b/baselines/baseline_template/baseline_template/server.py deleted file mode 100644 index 2fd7d42cde5a..000000000000 --- a/baselines/baseline_template/baseline_template/server.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Create global evaluation function. - -Optionally, also define a new Server class (please note this is not needed in most -settings). -""" diff --git a/baselines/baseline_template/baseline_template/strategy.py b/baselines/baseline_template/baseline_template/strategy.py deleted file mode 100644 index 17436c401c30..000000000000 --- a/baselines/baseline_template/baseline_template/strategy.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Optionally define a custom strategy. - -Needed only when the strategy is not yet implemented in Flower or because you want to -extend or modify the functionality of an existing strategy. -""" diff --git a/baselines/baseline_template/baseline_template/utils.py b/baselines/baseline_template/baseline_template/utils.py deleted file mode 100644 index 9a831719d623..000000000000 --- a/baselines/baseline_template/baseline_template/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Define any utility function. - -They are not directly relevant to the other (more FL specific) python modules. For -example, you may define here things like: loading a model from a checkpoint, saving -results, plotting. -""" diff --git a/baselines/baseline_template/pyproject.toml b/baselines/baseline_template/pyproject.toml deleted file mode 100644 index 31f1ee7bfe6d..000000000000 --- a/baselines/baseline_template/pyproject.toml +++ /dev/null @@ -1,137 +0,0 @@ -[build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.masonry.api" - -[tool.poetry] -name = "" # <----- Ensure it matches the name of your baseline directory containing all the source code -version = "1.0.0" -description = "Flower Baselines" -license = "Apache-2.0" -authors = ["The Flower Authors "] -readme = "README.md" -homepage = "https://flower.ai" -repository = "https://github.com/adap/flower" -documentation = "https://flower.ai" -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", -] - -[tool.poetry.dependencies] -python = ">=3.8.15, <3.12.0" # don't change this -flwr = { extras = ["simulation"], version = "1.5.0" } -hydra-core = "1.3.2" # don't change this - -[tool.poetry.dev-dependencies] -isort = "==5.13.2" -black = "==24.2.0" -docformatter = "==1.7.5" -mypy = "==1.4.1" -pylint = "==2.8.2" -flake8 = "==3.9.2" -pytest = "==6.2.4" -pytest-watch = "==4.2.0" -ruff = "==0.0.272" -types-requests = "==2.27.7" - -[tool.isort] -line_length = 88 -indent = " " -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true - -[tool.black] -line-length = 88 -target-version = ["py38", "py39", "py310", "py311"] - -[tool.pytest.ini_options] -minversion = "6.2" -addopts = "-qq" -testpaths = [ - "flwr_baselines", -] - -[tool.mypy] -ignore_missing_imports = true -strict = false -plugins = "numpy.typing.mypy_plugin" - -[tool.pylint."MESSAGES CONTROL"] -disable = "bad-continuation,duplicate-code,too-few-public-methods,useless-import-alias" -good-names = "i,j,k,_,x,y,X,Y" -signature-mutators = "hydra.main.main" - -[tool.pylint.typecheck] -generated-members = "numpy.*, torch.*, tensorflow.*" - -[[tool.mypy.overrides]] -module = [ - "importlib.metadata.*", - "importlib_metadata.*", -] -follow_imports = "skip" -follow_imports_for_stubs = true -disallow_untyped_calls = false - -[[tool.mypy.overrides]] -module = "torch.*" -follow_imports = "skip" -follow_imports_for_stubs = true - -[tool.docformatter] -wrap-summaries = 88 -wrap-descriptions = 88 - -[tool.ruff] -target-version = "py38" -line-length = 88 -select = ["D", "E", "F", "W", "B", "ISC", "C4"] -fixable = ["D", "E", "F", "W", "B", "ISC", "C4"] -ignore = ["B024", "B027"] -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", - "proto", -] - -[tool.ruff.pydocstyle] -convention = "numpy" diff --git a/baselines/dev/create-baseline.sh b/baselines/dev/create-baseline.sh deleted file mode 100755 index 53cd79c569aa..000000000000 --- a/baselines/dev/create-baseline.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# This script duplicates the `baseline_template` directory and changes its name -# to the one you specify when running this script. That name is also used to -# rename the subdirectory inside your new baseline directory as well as to set -# the Python package name that Poetry will build - -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ - -template="baseline_template" -name=$1 - -# copying directory -echo "Copying '$template' and renaming it to '$name'" -cp -r $template $name - -# renaming sub-directory -echo "Renaming sub-directory as '$name'" -mv $name/$template $name/$name - -# adjusting package name in pyproject.toml -cd $name -if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -e "s//$name/" pyproject.toml -else - sed -i -e "s//$name/" pyproject.toml -fi - -echo "!!! Your directory for your baseline '$name' is ready." From 1c01e7fe0147d4e446b8e6b003cab424ce3394d2 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 29 Aug 2024 13:26:53 +0200 Subject: [PATCH 144/188] docs(baselines) Update baselines contribution docs (#3995) --- baselines/README.md | 34 +++--- .../source/how-to-contribute-baselines.rst | 42 ++++--- baselines/doc/source/how-to-use-baselines.rst | 114 +++++++++++------- 3 files changed, 112 insertions(+), 78 deletions(-) diff --git a/baselines/README.md b/baselines/README.md index 3a84df02d8de..75bcccb68b2a 100644 --- a/baselines/README.md +++ b/baselines/README.md @@ -1,10 +1,9 @@ # Flower Baselines +> [!NOTE] > We are changing the way we structure the Flower baselines. While we complete the transition to the new format, you can still find the existing baselines in the `flwr_baselines` directory. Currently, you can make use of baselines for [FedAvg](https://github.com/adap/flower/tree/main/baselines/flwr_baselines/flwr_baselines/publications/fedavg_mnist), [FedOpt](https://github.com/adap/flower/tree/main/baselines/flwr_baselines/flwr_baselines/publications/adaptive_federated_optimization), and [LEAF-FEMNIST](https://github.com/adap/flower/tree/main/baselines/flwr_baselines/flwr_baselines/publications/leaf/femnist). -> The documentation below has been updated to reflect the new way of using Flower baselines. - ## Structure @@ -15,17 +14,15 @@ baselines// β”œβ”€β”€ README.md β”œβ”€β”€ pyproject.toml └── - β”œβ”€β”€ *.py # several .py files including main.py and __init__.py - └── conf - └── *.yaml # one or more Hydra config files + └── *.py # several .py files ``` -Please note that some baselines might include additional files (e.g. a `requirements.txt`) or a hierarchy of `.yaml` files for [Hydra](https://hydra.cc/). ## Running the baselines -Each baseline is self-contained in its own directory. Furthermore, each baseline defines its own Python environment using [Poetry](https://python-poetry.org/docs/) via a `pyproject.toml` file and [`pyenv`](https://github.com/pyenv/pyenv). If you haven't setup `Poetry` and `pyenv` already on your machine, please take a look at the [Documentation](https://flower.ai/docs/baselines/how-to-use-baselines.html#setting-up-your-machine) for a guide on how to do so. +> [!NOTE] +> We are in the process of migrating all baselines to use `flwr run`. Those baselines that remain using the previous system (i.e. using [Poetry](https://python-poetry.org/), [Hydra](https://hydra.cc/) and [start_simulation](https://flower.ai/docs/framework/ref-api/flwr.simulation.start_simulation.html)) might require you to first setup `Poetry` and `pyenv` already on your machine, please take a look at the [Documentation](https://flower.ai/docs/baselines/how-to-use-baselines.html#setting-up-your-machine) for a guide on how to do so. -Assuming `pyenv` and `Poetry` are already installed on your system. Running a baseline can be done by: +Each baseline is self-contained in its own directory. To run a baseline: 1. Cloning the flower repository @@ -34,11 +31,7 @@ Assuming `pyenv` and `Poetry` are already installed on your system. Running a ba ``` 2. Navigate inside the directory of the baseline you'd like to run. -3. Follow the `[Environment Setup]` instructions in the `README.md`. In most cases this will require you to just do: - - ```bash - poetry install - ``` +3. Follow the `[Environment Setup]` instructions in the `README.md`. 4. Run the baseline as indicated in the `[Running the Experiments]` section in the `README.md` or in the `[Expected Results]` section to reproduce the experiments in the paper. @@ -46,17 +39,22 @@ Assuming `pyenv` and `Poetry` are already installed on your system. Running a ba Do you have a new federated learning paper and want to add a new baseline to Flower? Or do you want to add an experiment to an existing baseline paper? Great, we really appreciate your contribution !! +> [!TIP] +> A more verbose version of these steps can be found in the [Flower Baselines documentation](https://flower.ai/docs/baselines/how-to-contribute-baselines.html). + The steps to follow are: +1. Create a new Python 3.10 environment and install Flower (`pip install flwr`) 1. Fork the Flower repo and clone it into your machine. -2. Navigate to the `baselines/` directory, choose a single-word (and **lowercase**) name for your baseline, and from there run: +2. Navigate to the `baselines/` directory, from there and with your environment activated, run: ```bash - # This will create a new directory with the same structure as `baseline_template`. - ./dev/create-baseline.sh + # Choose option "Flower Baseline" when prompted + flwr new ``` -3. Then, go inside your baseline directory and continue with the steps detailed in `EXTENDED_README.md` and `README.md`. -4. Once your code is ready and you have checked that following the instructions in your `README.md` the Python environment can be created correctly and that running the code following your instructions can reproduce the experiments in the paper, you just need to create a Pull Request (PR). Then, the process to merge your baseline into the Flower repo will begin! +3. Then, go inside your baseline directory and continue with the steps detailed in the `README.md`. +4. Once your code is ready, check that you have completed all the sections in the `README.md` and that, if a new environment is created, your baseline still runs (i.e. play the role of a person running the baseline you want to contribute). +5. Create a Pull Request (PR). Then, the process to merge your baseline into the Flower repo will begin! Further resources: diff --git a/baselines/doc/source/how-to-contribute-baselines.rst b/baselines/doc/source/how-to-contribute-baselines.rst index b568e73f1c11..429ac714c1aa 100644 --- a/baselines/doc/source/how-to-contribute-baselines.rst +++ b/baselines/doc/source/how-to-contribute-baselines.rst @@ -6,16 +6,14 @@ Do you have a new federated learning paper and want to add a new baseline to Flo The goal of Flower Baselines is to reproduce experiments from popular papers to accelerate researchers by enabling faster comparisons to new strategies, datasets, models, and federated pipelines in general. Before you start to work on a new baseline or experiment, please check the `Flower Issues `_ or `Flower Pull Requests `_ to see if someone else is already working on it. Please open a new issue if you are planning to work on a new baseline or experiment with a short description of the corresponding paper and the experiment you want to contribute. +If you are proposing a brand new baseline, please indicate what experiments from the paper are planning to include. Requirements ------------ -Contributing a new baseline is really easy. You only have to make sure that your federated learning experiments are running with Flower and replicate the results of a paper. Flower baselines need to make use of: +Contributing a new baseline is really easy. You only have to make sure that your federated learning experiments run with Flower, use `Flower Datasets `_, and replicate the results of a paper. +Preferably, the baselines make use of PyTorch, but other ML frameworks are also welcome. The baselines are expected to run in a machine with Ubuntu 22.04, but if yours runs also on macOS even better! -* `Poetry `_ to manage the Python environment. -* `Hydra `_ to manage the configuration files for your experiments. - -You can find more information about how to setup Poetry in your machine in the ``EXTENDED_README.md`` that is generated when you prepare your baseline. Add a new Flower Baseline ------------------------- @@ -27,11 +25,18 @@ Let's say you want to contribute the code of your most recent Federated Learning #. **Get the Flower source code on your machine** #. Fork the Flower codebase: go to the `Flower GitHub repo `_ and fork the code (click the *Fork* button in the top-right corner and follow the instructions) #. Clone the (forked) Flower source code: :code:`git clone git@github.com:[your_github_username]/flower.git` - #. Open the code in your favorite editor. -#. **Use the provided script to create your baseline directory** - #. Navigate to the baselines directory and run :code:`./dev/create-baseline.sh fedawesome` - #. A new directory in :code:`baselines/fedawesome` is created. - #. Follow the instructions in :code:`EXTENDED_README.md` and :code:`README.md` in your baseline directory. +#. **Create a new baseline using the template** + #. Create a new Python environment with Python 3.10 (we recommend doing this with `pyenv `_) + #. Install flower with: :code:`pip install flwr`. + #. Navigate to the baselines directory and run: :code:`flwr new fedawesome`. When prompted, choose the option :code:`Flower Baseline`. + #. A new directory in :code:`baselines/fedawesome` is created with the structure needed for a Flower Baseline. + #. Follow the instructions in the :code:`README.md` in your baseline directory. + + .. tip:: + At this point, your baseline contains source code showing how a simple :code:`PyTorch+CIFAR10` project can be built with Flower. + You can run it directly by executing :code:`flwr run .` from inside the directory of your baseline. Update the code with that + needed to implement your baseline. + #. **Open a pull request** #. Stage your changes: :code:`git add .` #. Commit & push: :code:`git commit -m "Create new FedAwesome baseline" ; git push` @@ -49,15 +54,18 @@ Further reading: Usability --------- -Flower is known and loved for its usability. Therefore, make sure that your baseline or experiment can be executed with a single command such as: +Flower is known and loved for its usability. Therefore, make sure that your baseline or experiment can be executed with a single command after installing the baseline project: .. code-block:: bash - poetry run python -m .main - - # or, once sourced into your environment - python -m .main + # Install the baseline project + pip install -e . + + # Run the baseline using default config + flwr run . + + # Run the baseline overriding the config + flwr run . --run-config lr=0.01,num-server-rounds=200 -We provide you with a `template-baseline `_ to use as guidance when contributing your baseline. Having all baselines follow a homogenous structure helps users to tryout many baselines without the overheads of having to understand each individual codebase. Similarly, by using Hydra throughout, users will immediately know how to parameterise your experiments directly from the command line. -We look forward to your contribution! +We look forward to your contribution! \ No newline at end of file diff --git a/baselines/doc/source/how-to-use-baselines.rst b/baselines/doc/source/how-to-use-baselines.rst index 4704a9b6074e..ec65f8f7d5ee 100644 --- a/baselines/doc/source/how-to-use-baselines.rst +++ b/baselines/doc/source/how-to-use-baselines.rst @@ -5,7 +5,6 @@ Use Baselines We are changing the way we structure the Flower baselines. While we complete the transition to the new format, you can still find the existing baselines and use them: `baselines (old) `_. Currently, you can make use of baselines for `FedAvg `_, `FedOpt `_, and `LEAF-FEMNIST `_. - The documentation below has been updated to reflect the new way of using Flower baselines. Structure --------- @@ -15,87 +14,116 @@ All baselines are available in the directory `baselines / + β”œβ”€β”€ LICENSE β”œβ”€β”€ README.md - β”œβ”€β”€ pyproject.toml + β”œβ”€β”€ pyproject.toml # defines dependencies + β”œβ”€β”€ _static # optionally a directory to save plots └── - β”œβ”€β”€ *.py # several .py files including main.py and __init__.py - └── conf - └── *.yaml # one or more Hydra config files - -Please note that some baselines might include additional files (e.g. a :code:`requirements.txt`) or a hierarchy of :code:`.yaml` files for `Hydra `_. + └── *.py # several .py files Setting up your machine ----------------------- -.. note:: - Flower baselines are designed to run on Ubuntu 22.04. While a GPU is not required to run the baselines, some of the more computationally demanding ones do benefit from GPU acceleration. +.. tip:: + Flower baselines are designed to run on Ubuntu 22.04 and Python 3.10. While a GPU is not required to run the baselines, some of the more computationally demanding ones do benefit from GPU acceleration. + All baselines are expected to make use of `pyenv `_. -Common to all baselines is `Poetry `_, a tool to manage Python dependencies. Baselines also make use of `Pyenv `_. You'll need to install both on your system before running a baseline. What follows is a step-by-step guide on getting :code:`pyenv` and :code:`Poetry` installed on your system. +.. note:: + We are in the process of migrating all baselines to use `flwr run`. Those that haven't yet been migrated still make use of `Poetry `_, a tool to manage Python dependencies. + Identifying whether the baseline you want to run requires Poetry or not is easy: check if the `Environment Setup` section in the baseline readme mentions Poetry. + Follow the instructions later in this section if you need to setup Poetry in your system. -Let's begin by installing :code:`pyenv`. We'll be following the standard procedure. Please refer to the `pyenv docs `_ for alternative ways of installing it. +Let's begin by installing :code:`pyenv`. We'll be following the standard procedure. Please refer to the `pyenv docs `_ for alternative ways of installing it, including for platforms other than Ubuntu. .. code-block:: bash - # first install a few packages needed later for pyenv - sudo apt install build-essential zlib1g-dev libssl-dev libsqlite3-dev \ - libreadline-dev libbz2-dev libffi-dev liblzma-dev + # first install a few packages needed later for pyenv + sudo apt install build-essential zlib1g-dev libssl-dev libsqlite3-dev \ + libreadline-dev libbz2-dev libffi-dev liblzma-dev - # now clone pyenv into your home directory (this is the default way of installing pyenv) - git clone https://github.com/pyenv/pyenv.git ~/.pyenv + # now clone pyenv into your home directory (this is the default way of installing pyenv) + git clone https://github.com/pyenv/pyenv.git ~/.pyenv - # Then add pyenv to your path by adding the below to your .bashrc/.zshrc - export PYENV_ROOT="$HOME/.pyenv" - command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" - eval "$(pyenv init -)" + # Then add pyenv to your path by adding the below to your .bashrc/.zshrc + export PYENV_ROOT="$HOME/.pyenv" + command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" Verify your installation by opening a new terminal and .. code-block:: bash - # check python versions available - pyenv versions - # * system (...) # <-- it should just show one + # check python versions available + pyenv versions + # * system (...) # <-- it should just show one + +Then you can proceed and install any version of Python. Baselines use Python 3.10, so we'll be installing a recent version of it. + +.. code-block:: bash + + pyenv install 3.10.14 + # this will take a little while + # once done, you should see that that version is available + pyenv versions + # system + # * 3.10.14 # <-- you just installed this -Then you can proceed and install any version of Python. Most baselines currently use Python 3.10.6, so we'll be installing that one. +Next, let's install the :code:`virtualenv` plugin. Check `the documentation `_ for alternative installation methods. .. code-block:: bash - pyenv install 3.10.6 - # this will take a little while - # once done, you should see that that version is available - pyenv versions - # system - # * 3.10.6 # <-- you just installed this + # Clone `pyenv-virtualenv` + git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv + + # Restart your shell + exec "$SHELL" + -Now that we have :code:`pyenv` installed, we are ready to install :code:`poetry`. Installing Poetry can be done from a single command: +Using :code:`pyenv` +~~~~~~~~~~~~~~~~~~~ + +Creating a virtual environment can be done as follows: .. code-block:: bash - curl -sSL https://install.python-poetry.org | python3 - + # Create an environment for Python 3.10.14 named test-env + pyenv virtualenv 3.10.14 test-env + + # Then activate it + pyenv activate test-env + + # Deactivate it as follows + pyenv deactivate - # add to path by putting this line at the end of your .zshrc/.bashrc - export PATH="$HOME/.local/bin:$PATH" + +(optional) Setup Poetry +~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we have :code:`pyenv` installed, we are ready to install :code:`poetry`. It can be done from a single command: + +.. code-block:: bash + + curl -sSL https://install.python-poetry.org | python3 - + + # add to path by putting this line at the end of your .zshrc/.bashrc + export PATH="$HOME/.local/bin:$PATH" To install Poetry from source, to customise your installation, or to further integrate Poetry with your shell after installation, please check `the Poetry documentation `_. + Using a Flower Baseline ----------------------- -To use Flower Baselines you need first to install :code:`pyenv` and :code:`Poetry`, then: +To use Flower Baselines you need first to install :code:`pyenv` and, depending on the baselines, also :code:`Poetry`, then: 1. Clone the flower repository .. code-block:: bash - git clone https://github.com/adap/flower.git && cd flower + git clone https://github.com/adap/flower.git && cd flower 2. Navigate inside the directory of the baseline you'd like to run -3. Follow the :code:`[Environment Setup]` instructions in the :code:`README.md`. In most cases this will require you to just do: - -.. code-block:: bash - - poetry install - -4. Run the baseline as indicated in the :code:`[Running the Experiments]` section in the :code:`README.md` or in the `[Expected Results]` section to reproduce the experiments in the paper. +3. Follow the :code:`[Environment Setup]` instructions in the :code:`README.md`. +4. Run the baseline as indicated in the :code:`[Running the Experiments]` section in the :code:`README.md` or in the :code:`[Expected Results]` section to reproduce the experiments in the paper. From 0ea6c511526a7f6f27b028ab69dc7329e8071d7f Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 29 Aug 2024 14:23:00 +0200 Subject: [PATCH 145/188] refactor(framework) Reorganize `pyproject.toml` and telemetry (#4098) --- pyproject.toml | 10 +++++--- src/py/flwr/common/telemetry.py | 42 +++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d0138a5689b..6c974304e785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,14 +52,18 @@ exclude = [ ] [tool.poetry.scripts] +# `flwr` CLI flwr = "flwr.cli.app:app" -flower-superlink = "flwr.server:run_superlink" +# SuperExec (can run with either Deployment Engine or Simulation Engine) flower-superexec = "flwr.superexec:run_superexec" +# Simulation Engine +flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" +# Deployment Engine +flower-superlink = "flwr.server:run_superlink" flower-supernode = "flwr.client:run_supernode" -flower-client-app = "flwr.client:run_client_app" flower-server-app = "flwr.server:run_server_app" -flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" flwr-clientapp = "flwr.client.clientapp:flwr_clientapp" +flower-client-app = "flwr.client:run_client_app" # Deprecated, use `flower-supernode` [tool.poetry.dependencies] python = "^3.8" diff --git a/src/py/flwr/common/telemetry.py b/src/py/flwr/common/telemetry.py index 399f400b7edc..4c9f53ee1e17 100644 --- a/src/py/flwr/common/telemetry.py +++ b/src/py/flwr/common/telemetry.py @@ -132,14 +132,36 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A # Ping PING = auto() - # Client: start_client + # --- LEGACY FUNCTIONS ------------------------------------------------------------- + + # Legacy: `start_client` function START_CLIENT_ENTER = auto() START_CLIENT_LEAVE = auto() - # Server: start_server + # Legacy: `start_server` function START_SERVER_ENTER = auto() START_SERVER_LEAVE = auto() + # Legacy: `start_simulation` function + START_SIMULATION_ENTER = auto() + START_SIMULATION_LEAVE = auto() + + # --- `flwr` CLI ------------------------------------------------------------------- + + # Not yet implemented + + # --- SuperExec -------------------------------------------------------------------- + + # SuperExec + RUN_SUPEREXEC_ENTER = auto() + RUN_SUPEREXEC_LEAVE = auto() + + # --- Simulation Engine ------------------------------------------------------------ + + # Not yet implemented + + # --- Deployment Engine ------------------------------------------------------------ + # Driver API RUN_DRIVER_API_ENTER = auto() RUN_DRIVER_API_LEAVE = auto() @@ -152,10 +174,6 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A RUN_SUPERLINK_ENTER = auto() RUN_SUPERLINK_LEAVE = auto() - # Simulation - START_SIMULATION_ENTER = auto() - START_SIMULATION_LEAVE = auto() - # Driver: Driver DRIVER_CONNECT = auto() DRIVER_DISCONNECT = auto() @@ -164,10 +182,6 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A START_DRIVER_ENTER = auto() START_DRIVER_LEAVE = auto() - # flower-client-app - RUN_CLIENT_APP_ENTER = auto() - RUN_CLIENT_APP_LEAVE = auto() - # flower-server-app RUN_SERVER_APP_ENTER = auto() RUN_SERVER_APP_LEAVE = auto() @@ -176,9 +190,11 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A RUN_SUPERNODE_ENTER = auto() RUN_SUPERNODE_LEAVE = auto() - # SuperExec - RUN_SUPEREXEC_ENTER = auto() - RUN_SUPEREXEC_LEAVE = auto() + # --- DEPRECATED ------------------------------------------------------------------- + + # [DEPRECATED] CLI: `flower-client-app` + RUN_CLIENT_APP_ENTER = auto() + RUN_CLIENT_APP_LEAVE = auto() # Use the ThreadPoolExecutor with max_workers=1 to have a queue From b817b3b954fe7843f97fb2f85a6f38293f2151dd Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 29 Aug 2024 14:33:28 +0200 Subject: [PATCH 146/188] fix(*:skip) Fix Legacy Key Value Format in Ubuntu Dockerfile (#4090) Signed-off-by: Robert Steiner --- src/docker/base/ubuntu/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docker/base/ubuntu/Dockerfile b/src/docker/base/ubuntu/Dockerfile index 31cc8381b7c5..643308f324df 100644 --- a/src/docker/base/ubuntu/Dockerfile +++ b/src/docker/base/ubuntu/Dockerfile @@ -32,7 +32,7 @@ RUN apt-get update \ # Install PyEnv and Python ARG PYTHON_VERSION=3.11 ENV PYENV_ROOT=/root/.pyenv -ENV PATH $PYENV_ROOT/bin:$PATH +ENV PATH=$PYENV_ROOT/bin:$PATH # https://github.com/hadolint/hadolint/wiki/DL4006 SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash From ea2f4cea47599c2275023feeda9536eeb3ead533 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 29 Aug 2024 16:02:26 +0200 Subject: [PATCH 147/188] refactor(framework:skip) Reorder and remove unused events (#4105) --- src/py/flwr/common/telemetry.py | 26 +++++------------------- src/py/flwr/server/compat/app.py | 5 ----- src/py/flwr/server/driver/grpc_driver.py | 4 +--- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/py/flwr/common/telemetry.py b/src/py/flwr/common/telemetry.py index 4c9f53ee1e17..bb5747eca2a6 100644 --- a/src/py/flwr/common/telemetry.py +++ b/src/py/flwr/common/telemetry.py @@ -162,34 +162,18 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A # --- Deployment Engine ------------------------------------------------------------ - # Driver API - RUN_DRIVER_API_ENTER = auto() - RUN_DRIVER_API_LEAVE = auto() - - # Fleet API - RUN_FLEET_API_ENTER = auto() - RUN_FLEET_API_LEAVE = auto() - - # Driver API and Fleet API + # CLI: `flower-superlink` RUN_SUPERLINK_ENTER = auto() RUN_SUPERLINK_LEAVE = auto() - # Driver: Driver - DRIVER_CONNECT = auto() - DRIVER_DISCONNECT = auto() - - # Driver: start_driver - START_DRIVER_ENTER = auto() - START_DRIVER_LEAVE = auto() + # CLI: `flower-supernode` + RUN_SUPERNODE_ENTER = auto() + RUN_SUPERNODE_LEAVE = auto() - # flower-server-app + # CLI: `flower-server-app` RUN_SERVER_APP_ENTER = auto() RUN_SERVER_APP_LEAVE = auto() - # SuperNode - RUN_SUPERNODE_ENTER = auto() - RUN_SUPERNODE_LEAVE = auto() - # --- DEPRECATED ------------------------------------------------------------------- # [DEPRECATED] CLI: `flower-client-app` diff --git a/src/py/flwr/server/compat/app.py b/src/py/flwr/server/compat/app.py index e978359fa828..1d3e5024ba90 100644 --- a/src/py/flwr/server/compat/app.py +++ b/src/py/flwr/server/compat/app.py @@ -18,7 +18,6 @@ from logging import INFO from typing import Optional -from flwr.common import EventType, event from flwr.common.logger import log from flwr.server.client_manager import ClientManager from flwr.server.history import History @@ -65,8 +64,6 @@ def start_driver( # pylint: disable=too-many-arguments, too-many-locals hist : flwr.server.history.History Object containing training and evaluation metrics. """ - event(EventType.START_DRIVER_ENTER) - # Initialize the Driver API server and config initialized_server, initialized_config = init_defaults( server=server, @@ -96,6 +93,4 @@ def start_driver( # pylint: disable=too-many-arguments, too-many-locals f_stop.set() thread.join() - event(EventType.START_SERVER_LEAVE) - return hist diff --git a/src/py/flwr/server/driver/grpc_driver.py b/src/py/flwr/server/driver/grpc_driver.py index 80ce9623ab3f..ea6d1c9ea3e5 100644 --- a/src/py/flwr/server/driver/grpc_driver.py +++ b/src/py/flwr/server/driver/grpc_driver.py @@ -21,7 +21,7 @@ import grpc -from flwr.common import DEFAULT_TTL, EventType, Message, Metadata, RecordSet, event +from flwr.common import DEFAULT_TTL, Message, Metadata, RecordSet from flwr.common.grpc import create_channel from flwr.common.logger import log from flwr.common.serde import ( @@ -94,7 +94,6 @@ def _connect(self) -> None: This will not call GetRun. """ - event(EventType.DRIVER_CONNECT) if self._is_connected: log(WARNING, "Already connected") return @@ -108,7 +107,6 @@ def _connect(self) -> None: def _disconnect(self) -> None: """Disconnect from the Driver API.""" - event(EventType.DRIVER_DISCONNECT) if not self._is_connected: log(DEBUG, "Already disconnected") return From 5c248330fbada244edbc01bfd9320396fd1a0d8b Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Thu, 29 Aug 2024 16:18:48 +0200 Subject: [PATCH 148/188] ci(*:skip) Add Robert and Danny as codeowners (#4100) --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5270bf89ae33..ce280c6bd2d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,3 +27,8 @@ README.md @jafermarq @tanertopal @danieljanes # GitHub Actions and Workflows /.github/workflows @Robert-Steiner @tanertopal @danieljanes /.github/actions @Robert-Steiner @tanertopal @danieljanes + +# Docker-related files +/.devcontainer @Robert-Steiner @Moep90 +**/Dockerfile @Robert-Steiner @Moep90 +**/*.Dockerfile @Robert-Steiner @Moep90 From 807d0586317a7baea1964de98249561d6034eee6 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Thu, 29 Aug 2024 15:25:20 +0100 Subject: [PATCH 149/188] fix(framework:skip) Allow retry when SuperLink is not reachable (#4106) --- .../grpc_rere_client/client_interceptor.py | 17 ++++++++++---- .../client_interceptor_test.py | 23 ++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor.py b/src/py/flwr/client/grpc_rere_client/client_interceptor.py index d2dded8a73d9..c16f911eb4c2 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor.py @@ -17,11 +17,13 @@ import base64 import collections +from logging import WARNING from typing import Any, Callable, Optional, Sequence, Tuple, Union import grpc from cryptography.hazmat.primitives.asymmetric import ec +from flwr.common.logger import log from flwr.common.secure_aggregation.crypto.symmetric_encryption import ( bytes_to_public_key, compute_hmac, @@ -151,8 +153,15 @@ def intercept_unary_unary( server_public_key_bytes = base64.urlsafe_b64decode( _get_value_from_tuples(_PUBLIC_KEY_HEADER, response.initial_metadata()) ) - self.server_public_key = bytes_to_public_key(server_public_key_bytes) - self.shared_secret = generate_shared_key( - self.private_key, self.server_public_key - ) + + if server_public_key_bytes != b"": + self.server_public_key = bytes_to_public_key(server_public_key_bytes) + else: + log(WARNING, "Can't get server public key, SuperLink may be offline") + + if self.server_public_key is not None: + self.shared_secret = generate_shared_key( + self.private_key, self.server_public_key + ) + return response diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py index 79416a8eb31b..155bae202720 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py @@ -164,7 +164,7 @@ def _init_retry_invoker() -> RetryInvoker: return RetryInvoker( wait_gen_factory=exponential, recoverable_exceptions=grpc.RpcError, - max_tries=None, + max_tries=1, max_time=None, on_giveup=lambda retry_state: ( log( @@ -415,6 +415,27 @@ def test_client_auth_get_run(self) -> None: assert actual_public_key == expected_public_key assert actual_hmac == expected_hmac + def test_without_servicer(self) -> None: + """Test client authentication without servicer.""" + # Prepare + self._server.stop(grace=None) + retry_invoker = _init_retry_invoker() + + # Execute and Assert + with self._connection( + self._address, + True, + retry_invoker, + GRPC_MAX_MESSAGE_LENGTH, + None, + (self._client_private_key, self._client_public_key), + ) as conn: + _, _, create_node, _, _, _ = conn + assert create_node is not None + create_node() + + assert self._servicer.received_client_metadata() is None + if __name__ == "__main__": unittest.main(verbosity=2) From 9456fbf9636aca5028f77bd63885d2b4c36d71e8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 29 Aug 2024 16:54:53 +0200 Subject: [PATCH 150/188] feat(framework) Add events for `flower-simulation` and `run_simulation` (#4107) Co-authored-by: jafermarq --- src/py/flwr/common/telemetry.py | 8 +++++++- src/py/flwr/simulation/run_simulation.py | 25 +++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/py/flwr/common/telemetry.py b/src/py/flwr/common/telemetry.py index bb5747eca2a6..981cfe79966a 100644 --- a/src/py/flwr/common/telemetry.py +++ b/src/py/flwr/common/telemetry.py @@ -158,7 +158,13 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: List[A # --- Simulation Engine ------------------------------------------------------------ - # Not yet implemented + # CLI: flower-simulation + CLI_FLOWER_SIMULATION_ENTER = auto() + CLI_FLOWER_SIMULATION_LEAVE = auto() + + # Python API: `run_simulation` + PYTHON_API_RUN_SIMULATION_ENTER = auto() + PYTHON_API_RUN_SIMULATION_LEAVE = auto() # --- Deployment Engine ------------------------------------------------------------ diff --git a/src/py/flwr/simulation/run_simulation.py b/src/py/flwr/simulation/run_simulation.py index 1eddd91108d8..38a6ee7d6c14 100644 --- a/src/py/flwr/simulation/run_simulation.py +++ b/src/py/flwr/simulation/run_simulation.py @@ -109,6 +109,11 @@ def run_simulation_from_cli() -> None: """Run Simulation Engine from the CLI.""" args = _parse_args_run_simulation().parse_args() + event( + EventType.CLI_FLOWER_SIMULATION_ENTER, + event_details={"backend": args.backend, "num-supernodes": args.num_supernodes}, + ) + # Add warnings for deprecated server_app and client_app arguments if args.server_app: warn_deprecated_feature( @@ -214,6 +219,7 @@ def run_simulation_from_cli() -> None: verbose_logging=args.verbose, server_app_run_config=fused_config, is_app=is_app, + exit_event=EventType.CLI_FLOWER_SIMULATION_LEAVE, ) @@ -267,6 +273,11 @@ def run_simulation( When disabled, only INFO, WARNING and ERROR log messages will be shown. If enabled, DEBUG-level logs will be displayed. """ + event( + EventType.PYTHON_API_RUN_SIMULATION_ENTER, + event_details={"backend": backend_name, "num-supernodes": num_supernodes}, + ) + if enable_tf_gpu_growth: warn_deprecated_feature_with_example( "Passing `enable_tf_gpu_growth=True` is deprecated.", @@ -284,6 +295,7 @@ def run_simulation( backend_config=backend_config, enable_tf_gpu_growth=enable_tf_gpu_growth, verbose_logging=verbose_logging, + exit_event=EventType.PYTHON_API_RUN_SIMULATION_LEAVE, ) @@ -367,6 +379,7 @@ def _main_loop( is_app: bool, enable_tf_gpu_growth: bool, run: Run, + exit_event: EventType, flwr_dir: Optional[str] = None, client_app: Optional[ClientApp] = None, client_app_attr: Optional[str] = None, @@ -374,7 +387,7 @@ def _main_loop( server_app_attr: Optional[str] = None, server_app_run_config: Optional[UserConfig] = None, ) -> None: - """Launch SuperLink with Simulation Engine, then ServerApp on a separate thread.""" + """Start ServerApp on a separate thread, then launch Simulation Engine.""" # Initialize StateFactory state_factory = StateFactory(":flwr-in-memory-state:") @@ -382,6 +395,7 @@ def _main_loop( # A Threading event to indicate if an exception was raised in the ServerApp thread server_app_thread_has_exception = threading.Event() serverapp_th = None + success = True try: # Register run log(DEBUG, "Pre-registering run with id %s", run.run_id) @@ -405,8 +419,7 @@ def _main_loop( enable_tf_gpu_growth=enable_tf_gpu_growth, ) - # SuperLink with Simulation Engine - event(EventType.RUN_SUPERLINK_ENTER) + # Start Simulation Engine vce.start_vce( num_supernodes=num_supernodes, client_app_attr=client_app_attr, @@ -424,13 +437,13 @@ def _main_loop( except Exception as ex: log(ERROR, "An exception occurred !! %s", ex) log(ERROR, traceback.format_exc()) + success = False raise RuntimeError("An error was encountered. Ending simulation.") from ex finally: # Trigger stop event f_stop.set() - - event(EventType.RUN_SUPERLINK_LEAVE) + event(exit_event, event_details={"success": success}) if serverapp_th: serverapp_th.join() if server_app_thread_has_exception.is_set(): @@ -442,6 +455,7 @@ def _main_loop( # pylint: disable=too-many-arguments,too-many-locals def _run_simulation( num_supernodes: int, + exit_event: EventType, client_app: Optional[ClientApp] = None, server_app: Optional[ServerApp] = None, backend_name: str = "ray", @@ -508,6 +522,7 @@ def _run_simulation( is_app, enable_tf_gpu_growth, run, + exit_event, flwr_dir, client_app, client_app_attr, From 4b59fe5d2e416f909103d88f87d27db180eb2314 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 29 Aug 2024 17:01:34 +0200 Subject: [PATCH 151/188] refactor(*:skip) Keep version of setuptools and pip in sync (#4093) Signed-off-by: Robert Steiner --- src/docker/base/alpine/Dockerfile | 8 +++++--- src/docker/base/ubuntu/Dockerfile | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/docker/base/alpine/Dockerfile b/src/docker/base/alpine/Dockerfile index 441e0fdd9b85..3e6a246e53c1 100644 --- a/src/docker/base/alpine/Dockerfile +++ b/src/docker/base/alpine/Dockerfile @@ -51,9 +51,11 @@ RUN pip install -U --no-cache-dir \ FROM python:${PYTHON_VERSION}-${DISTRO}${DISTRO_VERSION} AS base -# Upgrade system Python pip and setuptools -# hadolint ignore=DL3013 -RUN pip install -U --no-cache-dir pip setuptools +# Keep the version of system Python pip and setuptools in sync with those installed in the +# virtualenv. +ARG PIP_VERSION +ARG SETUPTOOLS_VERSION +RUN pip install -U --no-cache-dir pip==${PIP_VERSION} setuptools==${SETUPTOOLS_VERSION} # required by the grpc package RUN apk add --no-cache \ diff --git a/src/docker/base/ubuntu/Dockerfile b/src/docker/base/ubuntu/Dockerfile index 643308f324df..ddc662a0ae98 100644 --- a/src/docker/base/ubuntu/Dockerfile +++ b/src/docker/base/ubuntu/Dockerfile @@ -50,16 +50,16 @@ RUN LATEST=$(pyenv latest -k ${PYTHON_VERSION}) \ ENV PATH=/usr/local/bin/python/bin:$PATH -# Upgrade system Python pip and setuptools -# hadolint ignore=DL3013 -RUN pip install -U --no-cache-dir pip setuptools \ +ARG PIP_VERSION +ARG SETUPTOOLS_VERSION +# Keep the version of system Python pip and setuptools in sync with those installed in the +# virtualenv. +RUN pip install -U --no-cache-dir pip==${PIP_VERSION} setuptools==${SETUPTOOLS_VERSION} \ # Use a virtual environment to ensure that Python packages are installed in the same location # regardless of whether the subsequent image build is run with the app or the root user && python -m venv /python/venv ENV PATH=/python/venv/bin:$PATH -ARG PIP_VERSION -ARG SETUPTOOLS_VERSION ARG FLWR_VERSION ARG FLWR_PACKAGE=flwr RUN pip install -U --no-cache-dir \ From be870143eee648e98b3c691e949c5b4187b29dc5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 29 Aug 2024 17:21:28 +0200 Subject: [PATCH 152/188] break(framework) Remove CLI entry points exports (#4101) --- pyproject.toml | 10 +++++----- src/py/flwr/client/__init__.py | 4 ---- src/py/flwr/server/__init__.py | 4 ---- src/py/flwr/superexec/__init__.py | 6 ------ 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c974304e785..91b518af5e03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,15 +55,15 @@ exclude = [ # `flwr` CLI flwr = "flwr.cli.app:app" # SuperExec (can run with either Deployment Engine or Simulation Engine) -flower-superexec = "flwr.superexec:run_superexec" +flower-superexec = "flwr.superexec.app:run_superexec" # Simulation Engine flower-simulation = "flwr.simulation.run_simulation:run_simulation_from_cli" # Deployment Engine -flower-superlink = "flwr.server:run_superlink" -flower-supernode = "flwr.client:run_supernode" -flower-server-app = "flwr.server:run_server_app" +flower-superlink = "flwr.server.app:run_superlink" +flower-supernode = "flwr.client.supernode.app:run_supernode" +flower-server-app = "flwr.server.run_serverapp:run_server_app" flwr-clientapp = "flwr.client.clientapp:flwr_clientapp" -flower-client-app = "flwr.client:run_client_app" # Deprecated, use `flower-supernode` +flower-client-app = "flwr.client.supernode:run_client_app" # Deprecated [tool.poetry.dependencies] python = "^3.8" diff --git a/src/py/flwr/client/__init__.py b/src/py/flwr/client/__init__.py index 218f2fe20d62..dce3be9036bb 100644 --- a/src/py/flwr/client/__init__.py +++ b/src/py/flwr/client/__init__.py @@ -20,8 +20,6 @@ from .client import Client as Client from .client_app import ClientApp as ClientApp from .numpy_client import NumPyClient as NumPyClient -from .supernode import run_client_app as run_client_app -from .supernode import run_supernode as run_supernode from .typing import ClientFn as ClientFn from .typing import ClientFnExt as ClientFnExt @@ -32,8 +30,6 @@ "ClientFnExt", "NumPyClient", "mod", - "run_client_app", - "run_supernode", "start_client", "start_numpy_client", ] diff --git a/src/py/flwr/server/__init__.py b/src/py/flwr/server/__init__.py index 896b46298327..1dde95b6b047 100644 --- a/src/py/flwr/server/__init__.py +++ b/src/py/flwr/server/__init__.py @@ -17,14 +17,12 @@ from . import strategy from . import workflow as workflow -from .app import run_superlink as run_superlink from .app import start_server as start_server from .client_manager import ClientManager as ClientManager from .client_manager import SimpleClientManager as SimpleClientManager from .compat import LegacyContext as LegacyContext from .driver import Driver as Driver from .history import History as History -from .run_serverapp import run_server_app as run_server_app from .server import Server as Server from .server_app import ServerApp as ServerApp from .server_config import ServerConfig as ServerConfig @@ -40,8 +38,6 @@ "ServerAppComponents", "ServerConfig", "SimpleClientManager", - "run_server_app", - "run_superlink", "start_server", "strategy", "workflow", diff --git a/src/py/flwr/superexec/__init__.py b/src/py/flwr/superexec/__init__.py index a510c41f4182..0584ca663a02 100644 --- a/src/py/flwr/superexec/__init__.py +++ b/src/py/flwr/superexec/__init__.py @@ -13,9 +13,3 @@ # limitations under the License. # ============================================================================== """Flower SuperExec service.""" - -from .app import run_superexec as run_superexec - -__all__ = [ - "run_superexec", -] From 01d865e76cdc398c26c54a2fa5caf405fb045179 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 29 Aug 2024 17:37:37 +0200 Subject: [PATCH 153/188] feat(framework:skip) Move Docker Hub READMEs to flower repository (#4094) Signed-off-by: Robert Steiner Co-authored-by: Taner Topal --- .github/workflows/docker-readme.yml | 51 +++++++++++++++++++++++++++++ src/docker/base/README.md | 38 +++++++++++++++++++++ src/docker/clientapp/README.md | 22 +++++++++++++ src/docker/serverapp/README.md | 34 +++++++++++++++++++ src/docker/superexec/README.md | 26 +++++++++++++++ src/docker/superlink/README.md | 28 ++++++++++++++++ src/docker/supernode/README.md | 30 +++++++++++++++++ 7 files changed, 229 insertions(+) create mode 100644 .github/workflows/docker-readme.yml create mode 100644 src/docker/base/README.md create mode 100644 src/docker/clientapp/README.md create mode 100644 src/docker/serverapp/README.md create mode 100644 src/docker/superexec/README.md create mode 100644 src/docker/superlink/README.md create mode 100644 src/docker/supernode/README.md diff --git a/.github/workflows/docker-readme.yml b/.github/workflows/docker-readme.yml new file mode 100644 index 000000000000..29dd787d638e --- /dev/null +++ b/.github/workflows/docker-readme.yml @@ -0,0 +1,51 @@ +name: Update Docker READMEs + +on: + push: + branches: + - 'main' + paths: + - 'src/docker/**/README.md' + +jobs: + collect: + if: ${{ github.repository == 'adap/flower' }} + name: Collect Docker READMEs + runs-on: ubuntu-22.04 + timeout-minutes: 10 + outputs: + readme_files: ${{ steps.filter.outputs.readme_files }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + list-files: "json" + filters: | + readme: + - 'src/docker/**/README.md' + + update: + if: ${{ needs.collect.outputs.readme_files != '' && toJson(fromJson(needs.collect.outputs.readme_files)) != '[]' }} + name: Update Docker READMEs + runs-on: ubuntu-22.04 + timeout-minutes: 10 + needs: collect + strategy: + matrix: + readme_path: ${{ fromJSON(needs.collect.outputs.readme_files) }} + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - id: repository + run: echo "name=$(basename $(dirname ${{ matrix.readme_path }}))" >> "$GITHUB_OUTPUT" + + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0 + with: + repository: flwr/${{ steps.repository.outputs.name }} + readme-filepath: ${{ matrix.readme_path }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/src/docker/base/README.md b/src/docker/base/README.md new file mode 100644 index 000000000000..e61899525f19 --- /dev/null +++ b/src/docker/base/README.md @@ -0,0 +1,38 @@ +# Flower Base + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` + - nightly image uses Python 3.11 and Ubuntu 22.04 +- `1.10.0-py3.11-alpine3.19` +- `1.10.0-py3.11-ubuntu22.04` +- `1.10.0-py3.10-ubuntu22.04` +- `1.10.0-py3.9-ubuntu22.04` +- `1.10.0-py3.8-ubuntu22.04` +- `1.9.0-py3.11-alpine3.19` +- `1.9.0-py3.11-ubuntu22.04` +- `1.9.0-py3.10-ubuntu22.04` +- `1.9.0-py3.9-ubuntu22.04` +- `1.9.0-py3.8-ubuntu22.04` +- `1.8.0-py3.11-alpine3.19` +- `1.8.0-py3.11-ubuntu22.04` +- `1.8.0-py3.10-ubuntu22.04` +- `1.8.0-py3.9-ubuntu22.04` +- `1.8.0-py3.8-ubuntu22.04` diff --git a/src/docker/clientapp/README.md b/src/docker/clientapp/README.md new file mode 100644 index 000000000000..ac50d4dc9b8f --- /dev/null +++ b/src/docker/clientapp/README.md @@ -0,0 +1,22 @@ +# Flower ClientApp + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` diff --git a/src/docker/serverapp/README.md b/src/docker/serverapp/README.md new file mode 100644 index 000000000000..c4283fa51f00 --- /dev/null +++ b/src/docker/serverapp/README.md @@ -0,0 +1,34 @@ +# Flower ServerApp + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `1.10.0`, `1.10.0-py3.11-ubuntu22.04` +- `1.10.0-py3.10-ubuntu22.04` +- `1.10.0-py3.9-ubuntu22.04` +- `1.10.0-py3.8-ubuntu22.04` +- `1.9.0`, `1.9.0-py3.11-ubuntu22.04` +- `1.9.0-py3.10-ubuntu22.04` +- `1.9.0-py3.9-ubuntu22.04` +- `1.9.0-py3.8-ubuntu22.04` +- `1.8.0`, `1.8.0-py3.11-ubuntu22.04` +- `1.8.0-py3.10-ubuntu22.04` +- `1.8.0-py3.9-ubuntu22.04` +- `1.8.0-py3.8-ubuntu22.04` diff --git a/src/docker/superexec/README.md b/src/docker/superexec/README.md new file mode 100644 index 000000000000..03dcc2cba5c9 --- /dev/null +++ b/src/docker/superexec/README.md @@ -0,0 +1,26 @@ +# Flower SuperExec + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `1.10.0`, `1.10.0-py3.11-ubuntu22.04` +- `1.10.0-py3.10-ubuntu22.04` +- `1.10.0-py3.9-ubuntu22.04` +- `1.10.0-py3.8-ubuntu22.04` diff --git a/src/docker/superlink/README.md b/src/docker/superlink/README.md new file mode 100644 index 000000000000..3da3c16909b8 --- /dev/null +++ b/src/docker/superlink/README.md @@ -0,0 +1,28 @@ +# Flower SuperLink + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `1.10.0`, `1.10.0-py3.11-alpine3.19` +- `1.10.0-py3.11-ubuntu22.04` +- `1.9.0`, `1.9.0-py3.11-alpine3.19` +- `1.9.0-py3.11-ubuntu22.04` +- `1.8.0`, `1.8.0-py3.11-alpine3.19` +- `1.8.0-py3.11-ubuntu22.04` diff --git a/src/docker/supernode/README.md b/src/docker/supernode/README.md new file mode 100644 index 000000000000..defee36b35ae --- /dev/null +++ b/src/docker/supernode/README.md @@ -0,0 +1,30 @@ +# Flower SuperNode + +

+ + Flower Website + +

+ +## Quick reference + +- **Learn more:**
+ [Flower Docs](https://flower.ai/docs/framework/how-to-run-flower-using-docker.html) + +- **Where to get help:**
+ [Flower Discuss](https://discuss.flower.ai), [Slack](https://flower.ai/join-slack) or [GitHub](https://github.com/adap/flower) + +- **Supported architectures:**
+ `amd64`, `arm64v8` + +## Supported tags + +- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `1.10.0`, `1.10.0-py3.11-ubuntu22.04` +- `1.10.0-py3.10-ubuntu22.04` +- `1.10.0-py3.9-ubuntu22.04` +- `1.10.0-py3.8-ubuntu22.04` +- `1.9.0`, `1.9.0-py3.11-ubuntu22.04` +- `1.9.0-py3.10-ubuntu22.04` +- `1.9.0-py3.9-ubuntu22.04` +- `1.9.0-py3.8-ubuntu22.04` From 6d617154b99c9fab2c429259ad88ce3ac7008753 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 29 Aug 2024 22:15:45 +0200 Subject: [PATCH 154/188] docs(framework) Remove redundant JAX page (#4108) --- doc/source/conf.py | 1 + ...mple-jax-from-centralized-to-federated.rst | 282 ------------------ doc/source/index.rst | 1 - 3 files changed, 1 insertion(+), 283 deletions(-) delete mode 100644 doc/source/example-jax-from-centralized-to-federated.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index d3881325a5ce..de475748abb1 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -264,6 +264,7 @@ def find_test_modules(package_path): "example-mxnet-walk-through": "index.html", "ref-api/flwr.simulation.run_simulation_from_cli": "index.html", "contributor-how-to-create-new-messages": "index.html", + "example-jax-from-centralized-to-federated": "tutorial-quickstart-jax.html", } # -- Options for HTML output ------------------------------------------------- diff --git a/doc/source/example-jax-from-centralized-to-federated.rst b/doc/source/example-jax-from-centralized-to-federated.rst deleted file mode 100644 index 6b06a288a67a..000000000000 --- a/doc/source/example-jax-from-centralized-to-federated.rst +++ /dev/null @@ -1,282 +0,0 @@ -Example: JAX - Run JAX Federated -================================ - -This tutorial will show you how to use Flower to build a federated version of an existing JAX workload. -We are using JAX to train a linear regression model on a scikit-learn dataset. -We will structure the example similar to our `PyTorch - From Centralized To Federated `_ walkthrough. -First, we build a centralized training approach based on the `Linear Regression with JAX `_ tutorial`. -Then, we build upon the centralized training code to run the training in a federated fashion. - -Before we start building our JAX example, we need install the packages :code:`jax`, :code:`jaxlib`, :code:`scikit-learn`, and :code:`flwr`: - -.. code-block:: shell - - $ pip install jax jaxlib scikit-learn flwr - - -Linear Regression with JAX --------------------------- - -We begin with a brief description of the centralized training code based on a :code:`Linear Regression` model. -If you want a more in-depth explanation of what's going on then have a look at the official `JAX documentation `_. - -Let's create a new file called :code:`jax_training.py` with all the components required for a traditional (centralized) linear regression training. -First, the JAX packages :code:`jax` and :code:`jaxlib` need to be imported. In addition, we need to import :code:`sklearn` since we use :code:`make_regression` for the dataset and :code:`train_test_split` to split the dataset into a training and test set. -You can see that we do not yet import the :code:`flwr` package for federated learning. This will be done later. - -.. code-block:: python - - from typing import Dict, List, Tuple, Callable - import jax - import jax.numpy as jnp - from sklearn.datasets import make_regression - from sklearn.model_selection import train_test_split - - key = jax.random.PRNGKey(0) - -The :code:`load_data()` function loads the mentioned training and test sets. - -.. code-block:: python - - def load_data() -> Tuple[List[np.ndarray], List[np.ndarray], List[np.ndarray], List[np.ndarray]]: - # create our dataset and start with similar datasets for different clients - X, y = make_regression(n_features=3, random_state=0) - X, X_test, y, y_test = train_test_split(X, y) - return X, y, X_test, y_test - -The model architecture (a very simple :code:`Linear Regression` model) is defined in :code:`load_model()`. - -.. code-block:: python - - def load_model(model_shape) -> Dict: - # model weights - params = { - 'b' : jax.random.uniform(key), - 'w' : jax.random.uniform(key, model_shape) - } - return params - -We now need to define the training (function :code:`train()`), which loops over the training set and measures the loss (function :code:`loss_fn()`) for each batch of training examples. The loss function is separate since JAX takes derivatives with a :code:`grad()` function (defined in the :code:`main()` function and called in :code:`train()`). - -.. code-block:: python - - def loss_fn(params, X, y) -> Callable: - err = jnp.dot(X, params['w']) + params['b'] - y - return jnp.mean(jnp.square(err)) # mse - - def train(params, grad_fn, X, y) -> Tuple[np.array, float, int]: - num_examples = X.shape[0] - for epochs in range(10): - grads = grad_fn(params, X, y) - params = jax.tree_multimap(lambda p, g: p - 0.05 * g, params, grads) - loss = loss_fn(params,X, y) - # if epochs % 10 == 9: - # print(f'For Epoch {epochs} loss {loss}') - return params, loss, num_examples - -The evaluation of the model is defined in the function :code:`evaluation()`. The function takes all test examples and measures the loss of the linear regression model. - -.. code-block:: python - - def evaluation(params, grad_fn, X_test, y_test) -> Tuple[float, int]: - num_examples = X_test.shape[0] - err_test = loss_fn(params, X_test, y_test) - loss_test = jnp.mean(jnp.square(err_test)) - # print(f'Test loss {loss_test}') - return loss_test, num_examples - -Having defined the data loading, model architecture, training, and evaluation we can put everything together and train our model using JAX. As already mentioned, the :code:`jax.grad()` function is defined in :code:`main()` and passed to :code:`train()`. - -.. code-block:: python - - def main(): - X, y, X_test, y_test = load_data() - model_shape = X.shape[1:] - grad_fn = jax.grad(loss_fn) - print("Model Shape", model_shape) - params = load_model(model_shape) - params, loss, num_examples = train(params, grad_fn, X, y) - evaluation(params, grad_fn, X_test, y_test) - - - if __name__ == "__main__": - main() - -You can now run your (centralized) JAX linear regression workload: - -.. code-block:: python - - python3 jax_training.py - -So far this should all look fairly familiar if you've used JAX before. -Let's take the next step and use what we've built to create a simple federated learning system consisting of one server and two clients. - -JAX meets Flower ----------------- - -The concept of federating an existing workload is always the same and easy to understand. -We have to start a *server* and then use the code in :code:`jax_training.py` for the *clients* that are connected to the *server*. -The *server* sends model parameters to the clients. The *clients* run the training and update the parameters. -The updated parameters are sent back to the *server*, which averages all received parameter updates. -This describes one round of the federated learning process, and we repeat this for multiple rounds. - -Our example consists of one *server* and two *clients*. Let's set up :code:`server.py` first. The *server* needs to import the Flower package :code:`flwr`. -Next, we use the :code:`start_server` function to start a server and tell it to perform three rounds of federated learning. - -.. code-block:: python - - import flwr as fl - - if __name__ == "__main__": - fl.server.start_server(server_address="0.0.0.0:8080", config=fl.server.ServerConfig(num_rounds=3)) - -We can already start the *server*: - -.. code-block:: python - - python3 server.py - -Finally, we will define our *client* logic in :code:`client.py` and build upon the previously defined JAX training in :code:`jax_training.py`. -Our *client* needs to import :code:`flwr`, but also :code:`jax` and :code:`jaxlib` to update the parameters on our JAX model: - -.. code-block:: python - - from typing import Dict, List, Callable, Tuple - - import flwr as fl - import numpy as np - import jax - import jax.numpy as jnp - - import jax_training - - -Implementing a Flower *client* basically means implementing a subclass of either :code:`flwr.client.Client` or :code:`flwr.client.NumPyClient`. -Our implementation will be based on :code:`flwr.client.NumPyClient` and we'll call it :code:`FlowerClient`. -:code:`NumPyClient` is slightly easier to implement than :code:`Client` if you use a framework with good NumPy interoperability (like JAX) because it avoids some of the boilerplate that would otherwise be necessary. -:code:`FlowerClient` needs to implement four methods, two methods for getting/setting model parameters, one method for training the model, and one method for testing the model: - -#. :code:`set_parameters (optional)` - * set the model parameters on the local model that are received from the server - * transform parameters to NumPy :code:`ndarray`'s - * loop over the list of model parameters received as NumPy :code:`ndarray`'s (think list of neural network layers) -#. :code:`get_parameters` - * get the model parameters and return them as a list of NumPy :code:`ndarray`'s (which is what :code:`flwr.client.NumPyClient` expects) -#. :code:`fit` - * update the parameters of the local model with the parameters received from the server - * train the model on the local training set - * get the updated local model parameters and return them to the server -#. :code:`evaluate` - * update the parameters of the local model with the parameters received from the server - * evaluate the updated model on the local test set - * return the local loss to the server - -The challenging part is to transform the JAX model parameters from :code:`DeviceArray` to :code:`NumPy ndarray` to make them compatible with `NumPyClient`. - -The two :code:`NumPyClient` methods :code:`fit` and :code:`evaluate` make use of the functions :code:`train()` and :code:`evaluate()` previously defined in :code:`jax_training.py`. -So what we really do here is we tell Flower through our :code:`NumPyClient` subclass which of our already defined functions to call for training and evaluation. -We included type annotations to give you a better understanding of the data types that get passed around. - -.. code-block:: python - - - class FlowerClient(fl.client.NumPyClient): - """Flower client implementing using linear regression and JAX.""" - - def __init__( - self, - params: Dict, - grad_fn: Callable, - train_x: List[np.ndarray], - train_y: List[np.ndarray], - test_x: List[np.ndarray], - test_y: List[np.ndarray], - ) -> None: - self.params= params - self.grad_fn = grad_fn - self.train_x = train_x - self.train_y = train_y - self.test_x = test_x - self.test_y = test_y - - def get_parameters(self, config) -> Dict: - # Return model parameters as a list of NumPy ndarrays - parameter_value = [] - for _, val in self.params.items(): - parameter_value.append(np.array(val)) - return parameter_value - - def set_parameters(self, parameters: List[np.ndarray]) -> Dict: - # Collect model parameters and update the parameters of the local model - value=jnp.ndarray - params_item = list(zip(self.params.keys(),parameters)) - for item in params_item: - key = item[0] - value = item[1] - self.params[key] = value - return self.params - - - def fit( - self, parameters: List[np.ndarray], config: Dict - ) -> Tuple[List[np.ndarray], int, Dict]: - # Set model parameters, train model, return updated model parameters - print("Start local training") - self.params = self.set_parameters(parameters) - self.params, loss, num_examples = jax_training.train(self.params, self.grad_fn, self.train_x, self.train_y) - results = {"loss": float(loss)} - print("Training results", results) - return self.get_parameters(config={}), num_examples, results - - def evaluate( - self, parameters: List[np.ndarray], config: Dict - ) -> Tuple[float, int, Dict]: - # Set model parameters, evaluate the model on a local test dataset, return result - print("Start evaluation") - self.params = self.set_parameters(parameters) - loss, num_examples = jax_training.evaluation(self.params,self.grad_fn, self.test_x, self.test_y) - print("Evaluation accuracy & loss", loss) - return ( - float(loss), - num_examples, - {"loss": float(loss)}, - ) - -Having defined the federation process, we can run it. - -.. code-block:: python - - def main() -> None: - """Load data, start MNISTClient.""" - - # Load data - train_x, train_y, test_x, test_y = jax_training.load_data() - grad_fn = jax.grad(jax_training.loss_fn) - - # Load model (from centralized training) and initialize parameters - model_shape = train_x.shape[1:] - params = jax_training.load_model(model_shape) - - # Start Flower client - client = FlowerClient(params, grad_fn, train_x, train_y, test_x, test_y) - fl.client.start_client(server_address="0.0.0.0:8080", client.to_client()) - - if __name__ == "__main__": - main() - - -And that's it. You can now open two additional terminal windows and run - -.. code-block:: python - - python3 client.py - -in each window (make sure that the server is still running before you do so) and see your JAX project run federated learning across two clients. Congratulations! - -Next Steps ----------- - -The source code of this example was improved over time and can be found here: `Quickstart JAX `_. -Our example is somewhat over-simplified because both clients load the same dataset. - -You're now prepared to explore this topic further. How about using a more sophisticated model or using a different dataset? How about adding more clients? diff --git a/doc/source/index.rst b/doc/source/index.rst index 2a34693f7b26..4f6ad705e9bc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -102,7 +102,6 @@ Problem-oriented how-to guides show step-by-step how to achieve a specific goal. :caption: Legacy example guides example-pytorch-from-centralized-to-federated - example-jax-from-centralized-to-federated example-fedbn-pytorch-from-centralized-to-federated Explanations From 0f7bbe119e94146a07ede813a2a50f7e903b7878 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 30 Aug 2024 14:13:11 +0200 Subject: [PATCH 155/188] docs(framework) Update changelog for Flower 1.11.0 (#4110) --- doc/source/ref-changelog.md | 104 +++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/doc/source/ref-changelog.md b/doc/source/ref-changelog.md index 531afb9ada52..7fcea7edc729 100644 --- a/doc/source/ref-changelog.md +++ b/doc/source/ref-changelog.md @@ -1,6 +1,108 @@ # Changelog -## Unreleased +## v1.11.0 (2024-08-30) + +### Thanks to our contributors + +We would like to give our special thanks to all the contributors who made the new version of Flower possible (in `git shortlog` order): + +`Adam Narozniak`, `Charles Beauville`, `Chong Shen Ng`, `Daniel J. Beutel`, `Daniel Nata Nugraha`, `Danny`, `Edoardo Gabrielli`, `Heng Pan`, `Javier`, `Meng Yan`, `Michal Danilowski`, `Mohammad Naseri`, `Robert Steiner`, `Steve Laskaridis`, `Taner Topal`, `Yan Gao` + +### What's new? + +- **Deliver Flower App Bundle (FAB) to SuperLink and SuperNodes** ([#4006](https://github.com/adap/flower/pull/4006), [#3945](https://github.com/adap/flower/pull/3945), [#3999](https://github.com/adap/flower/pull/3999), [#4027](https://github.com/adap/flower/pull/4027), [#3851](https://github.com/adap/flower/pull/3851), [#3946](https://github.com/adap/flower/pull/3946), [#4003](https://github.com/adap/flower/pull/4003), [#4029](https://github.com/adap/flower/pull/4029), [#3942](https://github.com/adap/flower/pull/3942), [#3957](https://github.com/adap/flower/pull/3957), [#4020](https://github.com/adap/flower/pull/4020), [#4044](https://github.com/adap/flower/pull/4044), [#3852](https://github.com/adap/flower/pull/3852), [#4019](https://github.com/adap/flower/pull/4019), [#4031](https://github.com/adap/flower/pull/4031), [#4036](https://github.com/adap/flower/pull/4036), [#4049](https://github.com/adap/flower/pull/4049), [#4017](https://github.com/adap/flower/pull/4017), [#3943](https://github.com/adap/flower/pull/3943), [#3944](https://github.com/adap/flower/pull/3944), [#4011](https://github.com/adap/flower/pull/4011), [#3619](https://github.com/adap/flower/pull/3619)) + + Dynamic code updates are here! `flwr run` can now ship and install the latest version of your `ServerApp` and `ClientApp` to an already-running federation (SuperLink and SuperNodes). + + How does it work? `flwr run` bundles your Flower app into a single FAB (Flower App Bundle) file. It then ships this FAB file, via the SuperExec, to both the SuperLink and those SuperNodes that need it. This allows you to keep SuperExec, SuperLink and SuperNodes running as permanent infrastructure, and then ship code updates (including completely new projects!) dynamically. + + `flwr run` is all you need. + +- **Introduce isolated** `ClientApp` **execution** ([#3970](https://github.com/adap/flower/pull/3970), [#3976](https://github.com/adap/flower/pull/3976), [#4002](https://github.com/adap/flower/pull/4002), [#4001](https://github.com/adap/flower/pull/4001), [#4034](https://github.com/adap/flower/pull/4034), [#4037](https://github.com/adap/flower/pull/4037), [#3977](https://github.com/adap/flower/pull/3977), [#4042](https://github.com/adap/flower/pull/4042), [#3978](https://github.com/adap/flower/pull/3978), [#4039](https://github.com/adap/flower/pull/4039), [#4033](https://github.com/adap/flower/pull/4033), [#3971](https://github.com/adap/flower/pull/3971), [#4035](https://github.com/adap/flower/pull/4035), [#3973](https://github.com/adap/flower/pull/3973), [#4032](https://github.com/adap/flower/pull/4032)) + + The SuperNode can now run your `ClientApp` in a fully isolated way. In an enterprise deployment, this allows you to set strict limits on what the `ClientApp` can and cannot do. + + `flower-supernode` supports three `--isolation` modes: + + - Unset: The SuperNode runs the `ClientApp` in the same process (as in previous versions of Flower). This is the default mode. + - `--isolation=subprocess`: The SuperNode starts a subprocess to run the `ClientApp`. + - `--isolation=process`: The SuperNode expects an externally-managed process to run the `ClientApp`. This external process is not managed by the SuperNode, so it has to be started beforehand and terminated manually. The common way to use this isolation mode is via the new `flwr/clientapp` Docker image. + +- **Improve Docker support for enterprise deployments** ([#4050](https://github.com/adap/flower/pull/4050), [#4090](https://github.com/adap/flower/pull/4090), [#3784](https://github.com/adap/flower/pull/3784), [#3998](https://github.com/adap/flower/pull/3998), [#4094](https://github.com/adap/flower/pull/4094), [#3722](https://github.com/adap/flower/pull/3722)) + + Flower 1.11 ships many Docker improvements that are especially useful for enterprise deployments: + + - `flwr/supernode` comes with a new Alpine Docker image. + - `flwr/clientapp` is a new image to be used with the `--isolation=process` option. In this mode, SuperNode and `ClientApp` run in two different Docker containers. `flwr/supernode` (preferably the Alpine version) runs the long-running SuperNode with `--isolation=process`. `flwr/clientapp` runs the `ClientApp`. This is the recommended way to deploy Flower in enterprise settings. + - New all-in-one Docker Compose enables you to easily start a full Flower Deployment Engine on a single machine. + - Completely new Docker documentation: https://flower.ai/docs/framework/docker/index.html + +- **Improve SuperNode authentication** ([#4043](https://github.com/adap/flower/pull/4043), [#4047](https://github.com/adap/flower/pull/4047), [#4074](https://github.com/adap/flower/pull/4074)) + + SuperNode auth has been improved in several ways, including improved logging, improved testing, and improved error handling. + +- **Update** `flwr new` **templates** ([#3933](https://github.com/adap/flower/pull/3933), [#3894](https://github.com/adap/flower/pull/3894), [#3930](https://github.com/adap/flower/pull/3930), [#3931](https://github.com/adap/flower/pull/3931), [#3997](https://github.com/adap/flower/pull/3997), [#3979](https://github.com/adap/flower/pull/3979), [#3965](https://github.com/adap/flower/pull/3965), [#4013](https://github.com/adap/flower/pull/4013), [#4064](https://github.com/adap/flower/pull/4064)) + + All `flwr new` templates have been updated to show the latest recommended use of Flower APIs. + +- **Improve Simulation Engine** ([#4095](https://github.com/adap/flower/pull/4095), [#3913](https://github.com/adap/flower/pull/3913), [#4059](https://github.com/adap/flower/pull/4059), [#3954](https://github.com/adap/flower/pull/3954), [#4071](https://github.com/adap/flower/pull/4071), [#3985](https://github.com/adap/flower/pull/3985), [#3988](https://github.com/adap/flower/pull/3988)) + + The Flower Simulation Engine comes with several updates, including improved run config support, verbose logging, simulation backend configuration via `flwr run`, and more. + +- **Improve** `RecordSet` ([#4052](https://github.com/adap/flower/pull/4052), [#3218](https://github.com/adap/flower/pull/3218), [#4016](https://github.com/adap/flower/pull/4016)) + + `RecordSet` is the core object to exchange model parameters, configuration values and metrics between `ClientApp` and `ServerApp`. This release ships several smaller improvements to `RecordSet` and related `*Record` types. + +- **Update documentation** ([#3972](https://github.com/adap/flower/pull/3972), [#3925](https://github.com/adap/flower/pull/3925), [#4061](https://github.com/adap/flower/pull/4061), [#3984](https://github.com/adap/flower/pull/3984), [#3917](https://github.com/adap/flower/pull/3917), [#3900](https://github.com/adap/flower/pull/3900), [#4066](https://github.com/adap/flower/pull/4066), [#3765](https://github.com/adap/flower/pull/3765), [#4021](https://github.com/adap/flower/pull/4021), [#3906](https://github.com/adap/flower/pull/3906), [#4063](https://github.com/adap/flower/pull/4063), [#4076](https://github.com/adap/flower/pull/4076), [#3920](https://github.com/adap/flower/pull/3920), [#3916](https://github.com/adap/flower/pull/3916)) + + Many parts of the documentation, including the main tutorial, have been migrated to show new Flower APIs and other new Flower features like the improved Docker support. + +- **Migrate code example to use new Flower APIs** ([#3758](https://github.com/adap/flower/pull/3758), [#3701](https://github.com/adap/flower/pull/3701), [#3919](https://github.com/adap/flower/pull/3919), [#3918](https://github.com/adap/flower/pull/3918), [#3934](https://github.com/adap/flower/pull/3934), [#3893](https://github.com/adap/flower/pull/3893), [#3833](https://github.com/adap/flower/pull/3833), [#3922](https://github.com/adap/flower/pull/3922), [#3846](https://github.com/adap/flower/pull/3846), [#3777](https://github.com/adap/flower/pull/3777), [#3874](https://github.com/adap/flower/pull/3874), [#3873](https://github.com/adap/flower/pull/3873), [#3935](https://github.com/adap/flower/pull/3935), [#3754](https://github.com/adap/flower/pull/3754), [#3980](https://github.com/adap/flower/pull/3980), [#4089](https://github.com/adap/flower/pull/4089), [#4046](https://github.com/adap/flower/pull/4046), [#3314](https://github.com/adap/flower/pull/3314), [#3316](https://github.com/adap/flower/pull/3316), [#3295](https://github.com/adap/flower/pull/3295), [#3313](https://github.com/adap/flower/pull/3313)) + + Many code examples have been migrated to use new Flower APIs. + +- **Update Flower framework, framework internals and quality infrastructure** ([#4018](https://github.com/adap/flower/pull/4018), [#4053](https://github.com/adap/flower/pull/4053), [#4098](https://github.com/adap/flower/pull/4098), [#4067](https://github.com/adap/flower/pull/4067), [#4105](https://github.com/adap/flower/pull/4105), [#4048](https://github.com/adap/flower/pull/4048), [#4107](https://github.com/adap/flower/pull/4107), [#4069](https://github.com/adap/flower/pull/4069), [#3915](https://github.com/adap/flower/pull/3915), [#4101](https://github.com/adap/flower/pull/4101), [#4108](https://github.com/adap/flower/pull/4108), [#3914](https://github.com/adap/flower/pull/3914), [#4068](https://github.com/adap/flower/pull/4068), [#4041](https://github.com/adap/flower/pull/4041), [#4040](https://github.com/adap/flower/pull/4040), [#3986](https://github.com/adap/flower/pull/3986), [#4026](https://github.com/adap/flower/pull/4026), [#3961](https://github.com/adap/flower/pull/3961), [#3975](https://github.com/adap/flower/pull/3975), [#3983](https://github.com/adap/flower/pull/3983), [#4091](https://github.com/adap/flower/pull/4091), [#3982](https://github.com/adap/flower/pull/3982), [#4079](https://github.com/adap/flower/pull/4079), [#4073](https://github.com/adap/flower/pull/4073), [#4060](https://github.com/adap/flower/pull/4060), [#4106](https://github.com/adap/flower/pull/4106), [#4080](https://github.com/adap/flower/pull/4080), [#3974](https://github.com/adap/flower/pull/3974), [#3996](https://github.com/adap/flower/pull/3996), [#3991](https://github.com/adap/flower/pull/3991), [#3981](https://github.com/adap/flower/pull/3981), [#4093](https://github.com/adap/flower/pull/4093), [#4100](https://github.com/adap/flower/pull/4100), [#3939](https://github.com/adap/flower/pull/3939), [#3955](https://github.com/adap/flower/pull/3955), [#3940](https://github.com/adap/flower/pull/3940), [#4038](https://github.com/adap/flower/pull/4038)) + + As always, many parts of the Flower framework and quality infrastructure were improved and updated. + +### Deprecations + +- **Deprecate accessing `Context` via `Client.context`** ([#3797](https://github.com/adap/flower/pull/3797)) + + Now that both `client_fn` and `server_fn` receive a `Context` object, accessing `Context` via `Client.context` is deprecated. `Client.context` will be removed in a future release. If you need to access `Context` in your `Client` implementation, pass it manually when creating the `Client` instance in `client_fn`: + + ```python + def client_fn(context: Context) -> Client: + return FlowerClient(context).to_client() + ``` + +### Incompatible changes + +- **Update CLIs to accept an app directory instead of** `ClientApp` **and** `ServerApp` ([#3952](https://github.com/adap/flower/pull/3952), [#4077](https://github.com/adap/flower/pull/4077), [#3850](https://github.com/adap/flower/pull/3850)) + + The CLI commands `flower-supernode` and `flower-server-app` now accept an app directory as argument (instead of references to a `ClientApp` or `ServerApp`). An app directory is any directory containing a `pyproject.toml` file (with the appropriate Flower config fields set). The easiest way to generate a compatible project structure is to use `flwr new`. + +- **Disable** `flower-client-app` **CLI command** ([#4022](https://github.com/adap/flower/pull/4022)) + + `flower-client-app` has been disabled. Use `flower-supernode` instead. + +- **Use spaces instead of commas for separating config args** ([#4000](https://github.com/adap/flower/pull/4000)) + + When passing configs (run config, node config) to Flower, you now need to separate key-value pairs using spaces instead of commas. For example: + + ```bash + flwr run . --run-config "learning-rate=0.01 num_rounds=10" # Works + ``` + + Previously, you could pass configs using commas, like this: + + ```bash + flwr run . --run-config "learning-rate=0.01,num_rounds=10" # Doesn't work + ``` + +- **Remove** `flwr example` **CLI command** ([#4084](https://github.com/adap/flower/pull/4084)) + + The experimental `flwr example` CLI command has been removed. Use `flwr new` to generate a project and then run it using `flwr run`. ## v1.10.0 (2024-07-24) From e301ee24d9ef4672dbacb66f8d7ec7f56afcfa2d Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Fri, 30 Aug 2024 14:51:14 +0200 Subject: [PATCH 156/188] feat(framework) Increase dev version to Flower 1.12 (#4113) --- baselines/doc/source/conf.py | 2 +- doc/source/conf.py | 4 ++-- examples/doc/source/conf.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/baselines/doc/source/conf.py b/baselines/doc/source/conf.py index ecc3482c6fce..974c264a6220 100644 --- a/baselines/doc/source/conf.py +++ b/baselines/doc/source/conf.py @@ -37,7 +37,7 @@ author = "The Flower Authors" # The full version, including alpha/beta/rc tags -release = "1.10.0" +release = "1.11.0" # -- General configuration --------------------------------------------------- diff --git a/doc/source/conf.py b/doc/source/conf.py index de475748abb1..c645c556c603 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -90,10 +90,10 @@ author = "The Flower Authors" # The full version of the next release, including alpha/beta/rc tags -release = "1.11.0" +release = "1.12.0" # The current released version rst_prolog = """ -.. |stable_flwr_version| replace:: 1.10.0 +.. |stable_flwr_version| replace:: 1.11.0 .. |stable_flwr_superlink_docker_digest| replace:: 4b317d5b6030710b476f4dbfab2c3a33021ad40a0fcfa54d7edd45e0c51d889c .. |ubuntu_version| replace:: 22.04 .. |setuptools_version| replace:: 70.3.0 diff --git a/examples/doc/source/conf.py b/examples/doc/source/conf.py index 4e4b7b210051..3500d7f0b59c 100644 --- a/examples/doc/source/conf.py +++ b/examples/doc/source/conf.py @@ -29,7 +29,7 @@ author = "The Flower Authors" # The full version, including alpha/beta/rc tags -release = "1.11.0" +release = "1.12.0" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 91b518af5e03..6df9180ac3f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "flwr" -version = "1.11.0" +version = "1.12.0" description = "Flower: A Friendly Federated Learning Framework" license = "Apache-2.0" authors = ["The Flower Authors "] From e7487fc31099e0b0b6d1367bddef7f2eee611c99 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 30 Aug 2024 17:37:05 +0200 Subject: [PATCH 157/188] docs(framework:skip) Add flwr 1.11.0 to Docker READMEs (#4116) Signed-off-by: Robert Steiner --- src/docker/base/README.md | 7 ++++++- src/docker/clientapp/README.md | 6 +++++- src/docker/serverapp/README.md | 6 +++++- src/docker/superexec/README.md | 6 +++++- src/docker/superlink/README.md | 4 +++- src/docker/supernode/README.md | 7 ++++++- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/docker/base/README.md b/src/docker/base/README.md index e61899525f19..16822c18782e 100644 --- a/src/docker/base/README.md +++ b/src/docker/base/README.md @@ -19,8 +19,13 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` - nightly image uses Python 3.11 and Ubuntu 22.04 +- `1.11.0-py3.11-alpine3.19` +- `1.11.0-py3.11-ubuntu22.04` +- `1.11.0-py3.10-ubuntu22.04` +- `1.11.0-py3.9-ubuntu22.04` +- `1.11.0-py3.8-ubuntu22.04` - `1.10.0-py3.11-alpine3.19` - `1.10.0-py3.11-ubuntu22.04` - `1.10.0-py3.10-ubuntu22.04` diff --git a/src/docker/clientapp/README.md b/src/docker/clientapp/README.md index ac50d4dc9b8f..5827cb8974df 100644 --- a/src/docker/clientapp/README.md +++ b/src/docker/clientapp/README.md @@ -19,4 +19,8 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` +- `1.11.0`, `1.11.0-py3.11-ubuntu22.04` +- `1.11.0-py3.10-ubuntu22.04` +- `1.11.0-py3.9-ubuntu22.04` +- `1.11.0-py3.8-ubuntu22.04` diff --git a/src/docker/serverapp/README.md b/src/docker/serverapp/README.md index c4283fa51f00..f75704ad7bbb 100644 --- a/src/docker/serverapp/README.md +++ b/src/docker/serverapp/README.md @@ -19,7 +19,11 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` +- `1.11.0`, `1.11.0-py3.11-ubuntu22.04` +- `1.11.0-py3.10-ubuntu22.04` +- `1.11.0-py3.9-ubuntu22.04` +- `1.11.0-py3.8-ubuntu22.04` - `1.10.0`, `1.10.0-py3.11-ubuntu22.04` - `1.10.0-py3.10-ubuntu22.04` - `1.10.0-py3.9-ubuntu22.04` diff --git a/src/docker/superexec/README.md b/src/docker/superexec/README.md index 03dcc2cba5c9..c5c102313ccb 100644 --- a/src/docker/superexec/README.md +++ b/src/docker/superexec/README.md @@ -19,7 +19,11 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` +- `1.11.0`, `1.11.0-py3.11-ubuntu22.04` +- `1.11.0-py3.10-ubuntu22.04` +- `1.11.0-py3.9-ubuntu22.04` +- `1.11.0-py3.8-ubuntu22.04` - `1.10.0`, `1.10.0-py3.11-ubuntu22.04` - `1.10.0-py3.10-ubuntu22.04` - `1.10.0-py3.9-ubuntu22.04` diff --git a/src/docker/superlink/README.md b/src/docker/superlink/README.md index 3da3c16909b8..729a1f7ba7fb 100644 --- a/src/docker/superlink/README.md +++ b/src/docker/superlink/README.md @@ -19,7 +19,9 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` +- `1.11.0`, `1.11.0-py3.11-alpine3.19` +- `1.11.0-py3.11-ubuntu22.04` - `1.10.0`, `1.10.0-py3.11-alpine3.19` - `1.10.0-py3.11-ubuntu22.04` - `1.9.0`, `1.9.0-py3.11-alpine3.19` diff --git a/src/docker/supernode/README.md b/src/docker/supernode/README.md index defee36b35ae..becc2323ca2d 100644 --- a/src/docker/supernode/README.md +++ b/src/docker/supernode/README.md @@ -19,7 +19,12 @@ ## Supported tags -- `nightly`, `.dev` e.g. `1.11.0.dev20240724` +- `nightly`, `.dev` e.g. `1.12.0.dev20240830` +- `1.11.0`, `1.11.0-py3.11-alpine3.19` +- `1.11.0-py3.11-ubuntu22.04` +- `1.11.0-py3.10-ubuntu22.04` +- `1.11.0-py3.9-ubuntu22.04` +- `1.11.0-py3.8-ubuntu22.04` - `1.10.0`, `1.10.0-py3.11-ubuntu22.04` - `1.10.0-py3.10-ubuntu22.04` - `1.10.0-py3.9-ubuntu22.04` From 6c12082e66c9152fc6ca8c232931ae82d3bf6336 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Sat, 31 Aug 2024 12:26:25 +0200 Subject: [PATCH 158/188] docs(framework:skip) Update Docker docs for 1.11.0 (#4075) Signed-off-by: Robert Steiner Co-authored-by: Taner Topal --- doc/source/docker/index.rst | 5 +- doc/source/docker/run-as-subprocess.rst | 53 +++++ .../docker/tutorial-quickstart-docker.rst | 183 +++++++++++------- 3 files changed, 164 insertions(+), 77 deletions(-) create mode 100644 doc/source/docker/run-as-subprocess.rst diff --git a/doc/source/docker/index.rst b/doc/source/docker/index.rst index a070a47cb853..ac6124b4c138 100644 --- a/doc/source/docker/index.rst +++ b/doc/source/docker/index.rst @@ -33,11 +33,12 @@ Advanced Options set-environment-variables run-as-root-user + run-as-subprocess pin-version use-a-different-version -Run Flower Docker Compose -------------------------- +Run Flower using Docker Compose +------------------------------- .. toctree:: :maxdepth: 1 diff --git a/doc/source/docker/run-as-subprocess.rst b/doc/source/docker/run-as-subprocess.rst new file mode 100644 index 000000000000..f8c482f632a0 --- /dev/null +++ b/doc/source/docker/run-as-subprocess.rst @@ -0,0 +1,53 @@ +Run ClientApp as a Subprocess +============================= + +In this mode, the ClientApp is executed as a subprocess within the SuperNode Docker container, +rather than running in a separate container. This approach reduces the number of running containers, +which can be beneficial for environments with limited resources. However, it also means that the +ClientApp is no longer isolated from the SuperNode, which may introduce additional security +concerns. + +Prerequisites +------------- + +#. Before running the ClientApp as a subprocess, ensure that the FAB dependencies have been installed + in the SuperNode images. This can be done by extending the SuperNode image: + + .. code-block:: dockerfile + :caption: Dockerfile.supernode + :linenos: + :substitutions: + + FROM flwr/supernode:|stable_flwr_version| + + WORKDIR /app + COPY pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-supernode"] + +#. Next, build the SuperNode Docker image by running the following command in the directory where + Dockerfile is located: + + .. code-block:: shell + + $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . + + +Run the ClientApp as a Subprocess +--------------------------------- + +Start the SuperNode with the flag ``--isolation subprocess``, which tells the SuperNode to execute +the ClientApp as a subprocess: + +.. code-block:: shell + + $ docker run --rm \ + --detach \ + flwr_supernode:0.0.1 \ + --insecure \ + --superlink superlink:9092 \ + --node-config "partition-id=1 num-partitions=2" \ + --supernode-address localhost:9094 \ + --isolation subprocess diff --git a/doc/source/docker/tutorial-quickstart-docker.rst b/doc/source/docker/tutorial-quickstart-docker.rst index 29ae6d5f6a43..189d019cb097 100644 --- a/doc/source/docker/tutorial-quickstart-docker.rst +++ b/doc/source/docker/tutorial-quickstart-docker.rst @@ -66,8 +66,8 @@ Open your terminal and run: * ``docker run``: This tells Docker to run a container from an image. * ``--rm``: Remove the container once it is stopped or the command exits. * | ``-p 9091:9091 -p 9092:9092``: Map port ``9091`` and ``9092`` of the container to the same port of - | the host machine, allowing you to access the Driver API on ``http://localhost:9091`` and - | the Fleet API on ``http://localhost:9092``. + | the host machine, allowing other services to access the Driver API on + | ``http://localhost:9091`` and the Fleet API on ``http://localhost:9092``. * ``--network flwr-network``: Make the container join the network named ``flwr-network``. * ``--name superlink``: Assign the name ``superlink`` to the container. * ``--detach``: Run the container in the background, freeing up the terminal. @@ -79,32 +79,92 @@ Open your terminal and run: Step 3: Start the SuperNode --------------------------- -The SuperNode Docker image comes with a pre-installed version of Flower and serves as a base for -building your own SuperNode image. +Start two SuperNode containers. -#. Create a SuperNode Dockerfile called ``Dockerfile.supernode`` and paste the following code into it: +#. Start the first container: + + .. code-block:: bash + :substitutions: + + $ docker run --rm \ + -p 9094:9094 \ + --network flwr-network \ + --name supernode-1 \ + --detach \ + flwr/supernode:|stable_flwr_version| \ + --insecure \ + --superlink superlink:9092 \ + --node-config "partition-id=0 num-partitions=2" \ + --supernode-address 0.0.0.0:9094 \ + --isolation process + + .. dropdown:: Understand the command + + * ``docker run``: This tells Docker to run a container from an image. + * ``--rm``: Remove the container once it is stopped or the command exits. + * | ``-p 9094:9094``: Map port ``9094`` of the container to the same port of + | the host machine, allowing other services to access the SuperNode API on + | ``http://localhost:9094``. + * ``--network flwr-network``: Make the container join the network named ``flwr-network``. + * ``--name supernode-1``: Assign the name ``supernode-1`` to the container. + * ``--detach``: Run the container in the background, freeing up the terminal. + * | ``flwr/supernode:|stable_flwr_version|``: This is the name of the image to be run and the specific tag + | of the image. + * | ``--insecure``: This flag tells the container to operate in an insecure mode, allowing + | unencrypted communication. + * | ``--superlink superlink:9092``: Connect to the SuperLink's Fleet API at the address + | ``superlink:9092``. + * | ``--node-config "partition-id=0 num-partitions=2"``: Set the partition ID to ``0`` and the + | number of partitions to ``2`` for the SuperNode configuration. + * | ``--supernode-address 0.0.0.0:9094``: Set the address and port number that the SuperNode + | is listening on. + * | ``--isolation process``: Tells the SuperNode that the ClientApp is created by separate + | independent process. The SuperNode does not attempt to create it. + +#. Start the second container: + + .. code-block:: shell + :substitutions: + + $ docker run --rm \ + -p 9095:9095 \ + --network flwr-network \ + --name supernode-2 \ + --detach \ + flwr/supernode:|stable_flwr_version| \ + --insecure \ + --superlink superlink:9092 \ + --node-config "partition-id=1 num-partitions=2" \ + --supernode-address 0.0.0.0:9095 \ + --isolation process + +Step 4: Start the ClientApp +--------------------------- + +The ClientApp Docker image comes with a pre-installed version of Flower and serves as a base for +building your own ClientApp image. In order to install the FAB dependencies, you will need to create +a Dockerfile that extends the ClientApp image and installs the required dependencies. + +#. Create a ClientApp Dockerfile called ``Dockerfile.clientapp`` and paste the following code into it: .. code-block:: dockerfile - :caption: Dockerfile.supernode + :caption: Dockerfile.clientapp :linenos: :substitutions: - FROM flwr/supernode:|stable_flwr_version| + FROM flwr/clientapp:|stable_flwr_version| WORKDIR /app COPY pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ && python -m pip install -U --no-cache-dir . - COPY flower.quickstart-docker.1-0-0.fab . - RUN flwr install flower.quickstart-docker.1-0-0.fab - - ENTRYPOINT ["flower-supernode"] + ENTRYPOINT ["flwr-clientapp"] .. dropdown:: Understand the Dockerfile - * | :substitution-code:`FROM flwr/supernode:|stable_flwr_version|`: This line specifies that the Docker image - | to be built from is the ``flwr/supernode image``, version :substitution-code:`|stable_flwr_version|`. + * | :substitution-code:`FROM flwr/clientapp:|stable_flwr_version|`: This line specifies that the Docker image + | to be built from is the ``flwr/clientapp image``, version :substitution-code:`|stable_flwr_version|`. * | ``WORKDIR /app``: Set the working directory for the container to ``/app``. | Any subsequent commands that reference a directory will be relative to this directory. * | ``COPY pyproject.toml .``: Copy the ``pyproject.toml`` file @@ -116,51 +176,37 @@ building your own SuperNode image. | | The ``-U`` flag indicates that any existing packages should be upgraded, and | ``--no-cache-dir`` prevents pip from using the cache to speed up the installation. - * | ``COPY flower.quickstart-docker.1-0-0.fab .``: Copy the - | ``flower.quickstart-docker.1-0-0.fab`` file from the current working directory into - | the container's ``/app`` directory. - * | ``RUN flwr install flower.quickstart-docker.1-0-0.fab``: Run the ``flwr`` install command - | to install the Flower App Bundle locally. - * | ``ENTRYPOINT ["flower-supernode"]``: Set the command ``flower-supernode`` to be + * | ``ENTRYPOINT ["flwr-clientapp"]``: Set the command ``flwr-clientapp`` to be | the default command run when the container is started. .. important:: - Note that `flwr `__ is already installed in the ``flwr/supernode`` + Note that `flwr `__ is already installed in the ``flwr/clientapp`` base image, so only other package dependencies such as ``flwr-datasets``, ``torch``, etc., need to be installed. As a result, the ``flwr`` dependency is removed from the ``pyproject.toml`` after it has been copied into the Docker image (see line 5). -#. Build the Flower App Bundle (FAB): - - .. code-block:: bash - - $ flwr build - -#. Next, build the SuperNode Docker image by running the following command in the directory where - Dockerfile is located: +#. Next, build the ClientApp Docker image by running the following command in the directory where + the Dockerfile is located: .. code-block:: bash - $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . + $ docker build -f Dockerfile.clientapp -t flwr_clientapp:0.0.1 . .. note:: - The image name was set as ``flwr_supernode`` with the tag ``0.0.1``. Remember that + The image name was set as ``flwr_clientapp`` with the tag ``0.0.1``. Remember that these values are merely examples, and you can customize them according to your requirements. -#. Start the first SuperNode container: +#. Start the first ClientApp container: .. code-block:: bash $ docker run --rm \ --network flwr-network \ --detach \ - flwr_supernode:0.0.1 \ - --insecure \ - --superlink superlink:9092 \ - --node-config \ - partition-id=0,num-partitions=2 + flwr_clientapp:0.0.1 \ + --supernode supernode-1:9094 .. dropdown:: Understand the command @@ -168,35 +214,28 @@ building your own SuperNode image. * ``--rm``: Remove the container once it is stopped or the command exits. * ``--network flwr-network``: Make the container join the network named ``flwr-network``. * ``--detach``: Run the container in the background, freeing up the terminal. - * | ``flwr_supernode:0.0.1``: This is the name of the image to be run and the specific tag + * | ``flwr_clientapp:0.0.1``: This is the name of the image to be run and the specific tag | of the image. - * | ``--insecure``: This flag tells the container to operate in an insecure mode, allowing - | unencrypted communication. - * | ``--superlink superlink:9092``: Connect to the SuperLinks Fleet API on the address - | ``superlink:9092``. - * | ``--node-config partition-id=0,num-partitions=2``: Set the partition ID to ``0`` and the - | number of partitions to ``2`` for the SuperNode configuration. + * | ``--supernode supernode-1:9094``: Connect to the SuperNode's Fleet API at the address + | ``supernode-1:9094``. -#. Start the second SuperNode container: +#. Start the second ClientApp container: .. code-block:: shell $ docker run --rm \ --network flwr-network \ --detach \ - flwr_supernode:0.0.1 \ - --insecure \ - --superlink superlink:9092 \ - --node-config \ - partition-id=1,num-partitions=2 + flwr_clientapp:0.0.1 \ + --supernode supernode-2:9095 -Step 4: Start the SuperExec +Step 5: Start the SuperExec --------------------------- -The procedure for building and running a SuperExec image is almost identical to the SuperNode image. +The procedure for building and running a SuperExec image is almost identical to the ClientApp image. -Similar to the SuperNode image, the SuperExec Docker image comes with a pre-installed version of -Flower and serves as a base for building your own SuperExec image. +Similar to the ClientApp image, you will need to create a Dockerfile that extends the SuperExec +image and installs the required FAB dependencies. #. Create a SuperExec Dockerfile called ``Dockerfile.superexec`` and paste the following code in: @@ -254,8 +293,7 @@ Flower and serves as a base for building your own SuperExec image. --detach \ flwr_superexec:0.0.1 \ --insecure \ - --executor-config \ - superlink=\"superlink:9091\" + --executor-config superlink=\"superlink:9091\" .. dropdown:: Understand the command @@ -273,7 +311,7 @@ Flower and serves as a base for building your own SuperExec image. * | ``--executor-config superlink=\"superlink:9091\"``: Configure the SuperExec executor to | connect to the SuperLink running on port ``9091``. -Step 5: Run the Quickstart Project +Step 6: Run the Quickstart Project ---------------------------------- #. Add the following lines to the ``pyproject.toml``: @@ -297,7 +335,7 @@ Step 5: Run the Quickstart Project $ docker logs -f superexec -Step 6: Update the Application +Step 7: Update the Application ------------------------------ #. Change the application code. For example, change the ``seed`` in ``quickstart_docker/task.py`` @@ -310,39 +348,32 @@ Step 6: Update the Application partition_train_test = partition.train_test_split(test_size=0.2, seed=43) # ... -#. Stop the current SuperNode containers: +#. Stop the current ClientApp containers: .. code-block:: bash - $ docker stop $(docker ps -a -q --filter ancestor=flwr_supernode:0.0.1) + $ docker stop $(docker ps -a -q --filter ancestor=flwr_clientapp:0.0.1) -#. Rebuild the FAB and SuperNode image: +#. Rebuild the FAB and ClientApp image: .. code-block:: bash - $ flwr build - $ docker build -f Dockerfile.supernode -t flwr_supernode:0.0.1 . + $ docker build -f Dockerfile.clientapp -t flwr_clientapp:0.0.1 . -#. Launch two new SuperNode containers based on the newly built image: +#. Launch two new ClientApp containers based on the newly built image: .. code-block:: bash $ docker run --rm \ --network flwr-network \ --detach \ - flwr_supernode:0.0.1 \ - --insecure \ - --superlink superlink:9092 \ - --node-config \ - partition-id=0,num-partitions=2 + flwr_clientapp:0.0.1 \ + --supernode supernode-1:9094 $ docker run --rm \ --network flwr-network \ --detach \ - flwr_supernode:0.0.1 \ - --insecure \ - --superlink superlink:9092 \ - --node-config \ - partition-id=1,num-partitions=2 + flwr_clientapp:0.0.1 \ + --supernode supernode-2:9095 #. Run the updated project: @@ -350,14 +381,16 @@ Step 6: Update the Application $ flwr run . docker -Step 7: Clean Up +Step 8: Clean Up ---------------- Remove the containers and the bridge network: .. code-block:: bash - $ docker stop $(docker ps -a -q --filter ancestor=flwr_supernode:0.0.1) \ + $ docker stop $(docker ps -a -q --filter ancestor=flwr_clientapp:0.0.1) \ + supernode-1 \ + supernode-2 \ superexec \ superlink $ docker network rm flwr-network From 7215dfb7a7a65833471cdcec0689ec5f6503d6f7 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Sat, 31 Aug 2024 12:37:23 +0200 Subject: [PATCH 159/188] docs(framework:skip) Update Docker Compose docs for 1.11.0 (#4085) Signed-off-by: Robert Steiner --- .../tutorial-quickstart-docker-compose.rst | 56 +++++--- src/docker/complete/compose.yml | 124 +++++++++++------- src/docker/complete/with-tls.yml | 22 +++- 3 files changed, 135 insertions(+), 67 deletions(-) diff --git a/doc/source/docker/tutorial-quickstart-docker-compose.rst b/doc/source/docker/tutorial-quickstart-docker-compose.rst index 93a000295951..49cef55ec5a2 100644 --- a/doc/source/docker/tutorial-quickstart-docker-compose.rst +++ b/doc/source/docker/tutorial-quickstart-docker-compose.rst @@ -44,7 +44,7 @@ Step 1: Set Up Setting the ``PROJECT_DIR`` helps Docker Compose locate the ``pyproject.toml`` file, allowing it to install dependencies in the SuperExec and SuperNode images correctly. -Step 2: Run Flower in insecure mode +Step 2: Run Flower in Insecure Mode ----------------------------------- To begin, start Flower with the most basic configuration. In this setup, Flower @@ -230,7 +230,7 @@ Step 6: Run Flower with TLS [tool.flwr.federations.docker-compose-tls] address = "127.0.0.1:9093" - root-certificates = "superexec-certificates/ca.crt" + root-certificates = "../superexec-certificates/ca.crt" #. Restart the services with TLS enabled: @@ -248,43 +248,57 @@ Step 6: Run Flower with TLS Step 7: Add another SuperNode ----------------------------- -You can add more SuperNodes by duplicating the SuperNode definition in the ``compose.yml`` file. +You can add more SuperNodes and ClientApps by duplicating their definitions in the ``compose.yml`` +file. -Just make sure to give each new SuperNode service a unique service name like ``supernode-3``, ``supernode-4``, etc. +Just give each new SuperNode and ClientApp service a unique service name like ``supernode-3``, +``clientapp-3``, etc. In ``compose.yml``, add the following: .. code-block:: yaml :caption: compose.yml + :substitutions: - services: # other service definitions supernode-3: - user: root - deploy: - resources: - limits: - cpus: "2" + image: flwr/supernode:${FLWR_VERSION:-|stable_flwr_version|} command: + - --insecure - --superlink - superlink:9092 - - --insecure + - --supernode-address + - 0.0.0.0:9096 + - --isolation + - process + - --node-config + - "partition-id=1 num-partitions=2" depends_on: - superlink - volumes: - - apps-volume:/app/.flwr/apps/:ro + + clientapp-3: build: context: ${PROJECT_DIR:-.} dockerfile_inline: | - FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + FROM flwr/clientapp:${FLWR_VERSION:-|stable_flwr_version|} WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ && python -m pip install -U --no-cache-dir . - ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] + ENTRYPOINT ["flwr-clientapp"] + command: + - --supernode + - supernode-3:9096 + deploy: + resources: + limits: + cpus: "2" + stop_signal: SIGINT + depends_on: + - supernode-3 If you also want to enable TLS for the new SuperNodes, duplicate the SuperNode definition for each new SuperNode service in the ``with-tls.yml`` file. @@ -296,13 +310,18 @@ In ``with-tls.yml``, add the following: .. code-block:: yaml :caption: with-tls.yml - services: # other service definitions supernode-3: command: - --superlink - superlink:9092 + - --supernode-address + - 0.0.0.0:9096 + - --isolation + - process + - --node-config + - "partition-id=1 num-partitions=2" - --root-certificates - certificates/ca.crt secrets: @@ -315,14 +334,13 @@ Step 8: Persisting the SuperLink State and Enabling TLS To run Flower with persisted SuperLink state and enabled TLS, a slight change in the ``with-state.yml`` file is required: -#. Comment out the lines 3-5 and uncomment the lines 6-10: +#. Comment out the lines 2-4 and uncomment the lines 5-9: .. code-block:: yaml :caption: with-state.yml :linenos: - :emphasize-lines: 3-10 + :emphasize-lines: 2-9 - services: superlink: # command: # - --insecure diff --git a/src/docker/complete/compose.yml b/src/docker/complete/compose.yml index 90261249f322..60279adceb37 100644 --- a/src/docker/complete/compose.yml +++ b/src/docker/complete/compose.yml @@ -1,17 +1,16 @@ services: # create a SuperLink service superlink: - image: flwr/superlink:${FLWR_VERSION:-1.10.0} + image: flwr/superlink:${FLWR_VERSION:-1.11.0} command: - --insecure # create a SuperExec service superexec: - user: root build: context: ${PROJECT_DIR:-.} dockerfile_inline: | - FROM flwr/superexec:${FLWR_VERSION:-1.10.0} + FROM flwr/superexec:${FLWR_VERSION:-1.11.0} WORKDIR /app COPY --chown=app:app pyproject.toml . @@ -29,89 +28,122 @@ services: - superlink="superlink:9091" depends_on: - superlink - volumes: - - apps-volume:/app/.flwr/apps/:rw # create a two SuperNode service with different node configs supernode-1: - user: root - deploy: - resources: - limits: - cpus: "2" + image: flwr/supernode:${FLWR_VERSION:-1.11.0} command: + - --insecure - --superlink - superlink:9092 + - --supernode-address + - 0.0.0.0:9094 + - --isolation + - process + - --node-config + - "partition-id=0 num-partitions=2" + depends_on: + - superlink + + supernode-2: + image: flwr/supernode:${FLWR_VERSION:-1.11.0} + command: - --insecure + - --superlink + - superlink:9092 + - --supernode-address + - 0.0.0.0:9095 + - --isolation + - process + - --node-config + - "partition-id=1 num-partitions=2" depends_on: - superlink - volumes: - - apps-volume:/app/.flwr/apps/:ro + + # uncomment to add another SuperNode + # + # supernode-3: + # image: flwr/supernode:${FLWR_VERSION:-1.11.0} + # command: + # - --insecure + # - --superlink + # - superlink:9092 + # - --supernode-address + # - 0.0.0.0:9096 + # - --isolation + # - process + # - --node-config + # - "partition-id=1 num-partitions=2" + # depends_on: + # - superlink + + clientapp-1: build: context: ${PROJECT_DIR:-.} dockerfile_inline: | - FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ && python -m pip install -U --no-cache-dir . - ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] - - supernode-2: - user: root + ENTRYPOINT ["flwr-clientapp"] + command: + - --supernode + - supernode-1:9094 deploy: resources: limits: cpus: "2" - command: - - --superlink - - superlink:9092 - - --insecure + stop_signal: SIGINT depends_on: - - superlink - volumes: - - apps-volume:/app/.flwr/apps/:ro + - supernode-1 + + clientapp-2: build: context: ${PROJECT_DIR:-.} dockerfile_inline: | - FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ && python -m pip install -U --no-cache-dir . - ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=1,num-partitions=2"] + ENTRYPOINT ["flwr-clientapp"] + command: + - --supernode + - supernode-2:9095 + deploy: + resources: + limits: + cpus: "2" + stop_signal: SIGINT + depends_on: + - supernode-2 - # uncomment to add another supernode + # uncomment to add another ClientApp # - # supernode-3: - # user: root - # deploy: - # resources: - # limits: - # cpus: "2" - # command: - # - --superlink - # - superlink:9092 - # - --insecure - # depends_on: - # - superlink - # volumes: - # - apps-volume:/app/.flwr/apps/:ro + # clientapp-3: # build: # context: ${PROJECT_DIR:-.} # dockerfile_inline: | - # FROM flwr/supernode:${FLWR_VERSION:-1.10.0} + # FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} # WORKDIR /app # COPY --chown=app:app pyproject.toml . # RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ # && python -m pip install -U --no-cache-dir . - # ENTRYPOINT ["flower-supernode", "--node-config", "partition-id=0,num-partitions=2"] - -volumes: - apps-volume: + # ENTRYPOINT ["flwr-clientapp"] + # command: + # - --supernode + # - supernode-3:9096 + # deploy: + # resources: + # limits: + # cpus: "2" + # stop_signal: SIGINT + # depends_on: + # - supernode-3 diff --git a/src/docker/complete/with-tls.yml b/src/docker/complete/with-tls.yml index 1b8540e09b64..6cbeb2ba7397 100644 --- a/src/docker/complete/with-tls.yml +++ b/src/docker/complete/with-tls.yml @@ -17,7 +17,7 @@ services: - --executor - flwr.superexec.deployment:executor - --executor-config - - superlink="superlink:9091",root-certificates="certificates/superlink-ca.crt" + - superlink="superlink:9091" root-certificates="certificates/superlink-ca.crt" - --ssl-ca-certfile=certificates/ca.crt - --ssl-certfile=certificates/server.pem - --ssl-keyfile=certificates/server.key @@ -35,6 +35,12 @@ services: command: - --superlink - superlink:9092 + - --supernode-address + - 0.0.0.0:9094 + - --isolation + - process + - --node-config + - "partition-id=0 num-partitions=2" - --root-certificates - certificates/ca.crt secrets: @@ -45,18 +51,30 @@ services: command: - --superlink - superlink:9092 + - --supernode-address + - 0.0.0.0:9095 + - --isolation + - process + - --node-config + - "partition-id=1 num-partitions=2" - --root-certificates - certificates/ca.crt secrets: - source: superlink-ca-certfile target: /app/certificates/ca.crt - # uncomment to enable TLS on another supernode + # uncomment to enable TLS on another SuperNode # # supernode-3: # command: # - --superlink # - superlink:9092 + # - --supernode-address + # - 0.0.0.0:9096 + # - --isolation + # - process + # - --node-config + # - "partition-id=1 num-partitions=2" # - --root-certificates # - certificates/ca.crt # secrets: From 9bbfe9c6fee79c802f9aec8210c530bf598e1d9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:14:03 +0200 Subject: [PATCH 160/188] build(deps): bump actions/upload-artifact from 4.3.6 to 4.4.0 (#4122) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.6 to 4.4.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/834a144ee995460fba8ed112a2fc961b36a5ec5a...50769540e7f4bd5e21e526ee35c689e35e0d6874) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/_docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index a3373c6e93fa..ac88502b748a 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -122,7 +122,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: digests-${{ steps.build-id.outputs.id }}-${{ matrix.platform.name }} path: /tmp/digests/* From 24abe659769ae16ee03d9e545359bb1469f3d919 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Mon, 2 Sep 2024 22:24:31 +0800 Subject: [PATCH 161/188] fix(framework) Fix parsing `executor_config` if present (#4125) --- src/py/flwr/superexec/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/superexec/app.py b/src/py/flwr/superexec/app.py index 9510479ec8e1..67568b8378e0 100644 --- a/src/py/flwr/superexec/app.py +++ b/src/py/flwr/superexec/app.py @@ -56,7 +56,9 @@ def run_superexec() -> None: address=address, executor=_load_executor(args), certificates=certificates, - config=parse_config_args([args.executor_config]), + config=parse_config_args( + [args.executor_config] if args.executor_config else args.executor_config + ), ) grpc_servers = [superexec_server] From 5c921888382c69838e41bc362579804907f201f7 Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 2 Sep 2024 16:31:55 +0200 Subject: [PATCH 162/188] fix(examples) Update examples READMEs (#4117) --- examples/federated-kaplan-meier-fitter/README.md | 2 +- examples/federated-kaplan-meier-fitter/pyproject.toml | 2 +- examples/flower-secure-aggregation/README.md | 2 +- examples/flower-secure-aggregation/pyproject.toml | 2 +- examples/flowertune-llm/pyproject.toml | 2 +- examples/flowertune-vit/README.md | 2 +- examples/flowertune-vit/pyproject.toml | 2 +- examples/quickstart-fastai/pyproject.toml | 2 +- examples/quickstart-huggingface/pyproject.toml | 2 +- examples/quickstart-mlx/README.md | 2 +- examples/quickstart-mlx/pyproject.toml | 2 +- examples/quickstart-monai/README.md | 2 +- examples/quickstart-monai/pyproject.toml | 2 +- examples/quickstart-pytorch-lightning/README.md | 2 +- examples/quickstart-pytorch-lightning/pyproject.toml | 2 +- examples/quickstart-pytorch/README.md | 2 +- examples/quickstart-pytorch/pyproject.toml | 2 +- examples/quickstart-tensorflow/README.md | 2 +- examples/sklearn-logreg-mnist/README.md | 2 +- examples/sklearn-logreg-mnist/pyproject.toml | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/federated-kaplan-meier-fitter/README.md b/examples/federated-kaplan-meier-fitter/README.md index 1964ec4e5653..cc68a331bbba 100644 --- a/examples/federated-kaplan-meier-fitter/README.md +++ b/examples/federated-kaplan-meier-fitter/README.md @@ -69,7 +69,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,learning-rate=0.05 +flwr run . --run-config "num-server-rounds=5 learning-rate=0.05" ``` You can also check that the results match the centralized version. diff --git a/examples/federated-kaplan-meier-fitter/pyproject.toml b/examples/federated-kaplan-meier-fitter/pyproject.toml index 47cb0a4ba286..159ccc15efe4 100644 --- a/examples/federated-kaplan-meier-fitter/pyproject.toml +++ b/examples/federated-kaplan-meier-fitter/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Kaplan Meier Fitter with Flower" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets>=0.3.0", "numpy>=1.23.2", "pandas>=2.0.0", diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 9e92aed01d9e..0a9056263db3 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -57,7 +57,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example ```bash -flwr run . --run-config num-server-rounds=5,learning-rate=0.25 +flwr run . --run-config "num-server-rounds=5 learning-rate=0.25" ``` To adapt the example for a practial usage, set `is-demo=false` like shown below. You might want to adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. You can override those via `--run-config` as well. diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index d9be719653b0..6ac94253e839 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Secure Aggregation in Flower" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "torch==2.2.1", "torchvision==0.17.1", diff --git a/examples/flowertune-llm/pyproject.toml b/examples/flowertune-llm/pyproject.toml index 8171d7680620..20aa7267d9d5 100644 --- a/examples/flowertune-llm/pyproject.toml +++ b/examples/flowertune-llm/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "FlowerTune LLM: Federated LLM Fine-tuning with Flower" license = "Apache-2.0" dependencies = [ - "flwr-nightly[simulation]==1.11.0.dev20240826", + "flwr[simulation]==1.11.0", "flwr-datasets>=0.3.0", "trl==0.8.1", "bitsandbytes==0.43.0", diff --git a/examples/flowertune-vit/README.md b/examples/flowertune-vit/README.md index 9e2b0fd6b079..48327880f412 100644 --- a/examples/flowertune-vit/README.md +++ b/examples/flowertune-vit/README.md @@ -59,7 +59,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,batch-size=64 +flwr run . --run-config "num-server-rounds=5 batch-size=64" ``` Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 5x`ClientApp` will run in parallel in the available GPU. You can tweak the degree of parallelism by adjusting the settings of this federation in the `pyproject.toml`. diff --git a/examples/flowertune-vit/pyproject.toml b/examples/flowertune-vit/pyproject.toml index 0f11dc54c81a..d0feabc14212 100644 --- a/examples/flowertune-vit/pyproject.toml +++ b/examples/flowertune-vit/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Finetuning of a Vision Transformer with Flower" license = "Apache-2.0" dependencies = [ - "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr[simulation]==1.11.0", "flwr-datasets[vision]>=0.3.0", "torch==2.2.1", "torchvision==0.17.1", diff --git a/examples/quickstart-fastai/pyproject.toml b/examples/quickstart-fastai/pyproject.toml index 4d160bae0eec..25219ffcac4c 100644 --- a/examples/quickstart-fastai/pyproject.toml +++ b/examples/quickstart-fastai/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Learning with Fastai and Flower (Quickstart Example)" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "fastai==2.7.14", "torch==2.2.0", diff --git a/examples/quickstart-huggingface/pyproject.toml b/examples/quickstart-huggingface/pyproject.toml index af48b2429635..696f05b33ebf 100644 --- a/examples/quickstart-huggingface/pyproject.toml +++ b/examples/quickstart-huggingface/pyproject.toml @@ -12,7 +12,7 @@ authors = [ { name = "Kaushik Amar Das", email = "kaushik.das@iiitg.ac.in" }, ] dependencies = [ - "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr[simulation]==1.11.0", "flwr-datasets>=0.3.0", "torch==2.4.0", "transformers>=4.30.0,<5.0", diff --git a/examples/quickstart-mlx/README.md b/examples/quickstart-mlx/README.md index 95b9ccf605b5..ef28c3728279 100644 --- a/examples/quickstart-mlx/README.md +++ b/examples/quickstart-mlx/README.md @@ -58,7 +58,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,learning-rate=0.05 +flwr run . --run-config "num-server-rounds=5 learning-rate=0.05" ``` > \[!TIP\] diff --git a/examples/quickstart-mlx/pyproject.toml b/examples/quickstart-mlx/pyproject.toml index 36e39bcd6d78..459cac86f5d6 100644 --- a/examples/quickstart-mlx/pyproject.toml +++ b/examples/quickstart-mlx/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Learning with MLX and Flower (Quickstart Example)" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "mlx==0.16.0", "numpy==1.26.4", diff --git a/examples/quickstart-monai/README.md b/examples/quickstart-monai/README.md index c470a6a6c86f..8189a8e98406 100644 --- a/examples/quickstart-monai/README.md +++ b/examples/quickstart-monai/README.md @@ -70,7 +70,7 @@ flwr run . local-simulation-gpu You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,batch-size=32 +flwr run . --run-config "num-server-rounds=5 batch-size=32" ``` ### Run with the Deployment Engine diff --git a/examples/quickstart-monai/pyproject.toml b/examples/quickstart-monai/pyproject.toml index 6ecf5011d24f..daa92fc0387d 100644 --- a/examples/quickstart-monai/pyproject.toml +++ b/examples/quickstart-monai/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Learning with MONAI and Flower (Quickstart Example)" license = "Apache-2.0" dependencies = [ - "flwr-nightly[simulation]==1.11.0.dev20240823", + "flwr[simulation]==1.11.0", "flwr-datasets[vision]>=0.3.0", "monai==1.3.2", "filelock==3.15.4", diff --git a/examples/quickstart-pytorch-lightning/README.md b/examples/quickstart-pytorch-lightning/README.md index e520be856962..0aa34db9af75 100644 --- a/examples/quickstart-pytorch-lightning/README.md +++ b/examples/quickstart-pytorch-lightning/README.md @@ -52,7 +52,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,max-epochs=2 +flwr run . --run-config "num-server-rounds=5 max-epochs=2" ``` ### Run with the Deployment Engine diff --git a/examples/quickstart-pytorch-lightning/pyproject.toml b/examples/quickstart-pytorch-lightning/pyproject.toml index 482fc1356527..c5537ac6fcbe 100644 --- a/examples/quickstart-pytorch-lightning/pyproject.toml +++ b/examples/quickstart-pytorch-lightning/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Learning with PyTorch Lightning and Flower (Quickstart Example)" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "pytorch-lightning<2.0.0; sys_platform == 'darwin'", "pytorch-lightning==1.6.0; sys_platform != 'darwin'", diff --git a/examples/quickstart-pytorch/README.md b/examples/quickstart-pytorch/README.md index e37d49194b01..d07f83a7ea85 100644 --- a/examples/quickstart-pytorch/README.md +++ b/examples/quickstart-pytorch/README.md @@ -55,7 +55,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,learning-rate=0.05 +flwr run . --run-config "num-server-rounds=5 learning-rate=0.05" ``` > \[!TIP\] diff --git a/examples/quickstart-pytorch/pyproject.toml b/examples/quickstart-pytorch/pyproject.toml index 29414962ba6b..98f02626a429 100644 --- a/examples/quickstart-pytorch/pyproject.toml +++ b/examples/quickstart-pytorch/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "Federated Learning with PyTorch and Flower (Quickstart Example)" license = "Apache-2.0" dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "torch==2.2.1", "torchvision==0.17.1", diff --git a/examples/quickstart-tensorflow/README.md b/examples/quickstart-tensorflow/README.md index f1fa12a3393c..a162e756d799 100644 --- a/examples/quickstart-tensorflow/README.md +++ b/examples/quickstart-tensorflow/README.md @@ -56,7 +56,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,learning-rate=0.05 +flwr run . --run-config "num-server-rounds=5 learning-rate=0.05" ``` > \[!TIP\] diff --git a/examples/sklearn-logreg-mnist/README.md b/examples/sklearn-logreg-mnist/README.md index b56dbfc5dd3a..7c75e2ecfb85 100644 --- a/examples/sklearn-logreg-mnist/README.md +++ b/examples/sklearn-logreg-mnist/README.md @@ -55,7 +55,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: ```bash -flwr run . --run-config num-server-rounds=5,fraction-fit=0.25 +flwr run . --run-config "num-server-rounds=5 fraction-fit=0.25" ``` > \[!TIP\] diff --git a/examples/sklearn-logreg-mnist/pyproject.toml b/examples/sklearn-logreg-mnist/pyproject.toml index be1e4810b312..937f05e35eda 100644 --- a/examples/sklearn-logreg-mnist/pyproject.toml +++ b/examples/sklearn-logreg-mnist/pyproject.toml @@ -12,7 +12,7 @@ authors = [ { name = "Kaushik Amar Das", email = "kaushik.das@iiitg.ac.in" }, ] dependencies = [ - "flwr[simulation]>=1.10.0", + "flwr[simulation]>=1.11.0", "flwr-datasets[vision]>=0.3.0", "numpy<2.0.0", "scikit-learn~=1.2.2", From daaa54e78982839d927fad9d59cb4b57d61d2137 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Mon, 2 Sep 2024 15:37:49 +0100 Subject: [PATCH 163/188] fix(framework) Fix `FlowerTune` template (#4123) --- src/py/flwr/cli/new/new.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/py/flwr/cli/new/new.py b/src/py/flwr/cli/new/new.py index 9f2d32ddf99c..520f683a47d8 100644 --- a/src/py/flwr/cli/new/new.py +++ b/src/py/flwr/cli/new/new.py @@ -196,7 +196,6 @@ def new( f"{import_name}/client_app.py": { "template": "app/code/flwr_tune/client_app.py.tpl" }, - f"{import_name}/app.py": {"template": "app/code/flwr_tune/app.py.tpl"}, f"{import_name}/models.py": { "template": "app/code/flwr_tune/models.py.tpl" }, From 0f7c64ed2136f95de5afb472b8ed044f52d292d3 Mon Sep 17 00:00:00 2001 From: Taner Topal Date: Mon, 2 Sep 2024 19:07:04 +0200 Subject: [PATCH 164/188] ci(*:skip) Add Javier as codeowner to /benchmarks (#4126) --- .github/CODEOWNERS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ce280c6bd2d4..ccf031344f67 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,7 +7,10 @@ README.md @jafermarq @tanertopal @danieljanes # Flower Baselines -/baselines @jafermarq @tanertopal @danieljanes +/baselines @jafermarq @danieljanes + +# Flower Benchmarks +/benchmarks @jafermarq @danieljanes # Flower Datasets /datasets @jafermarq @tanertopal @danieljanes From 24e9af9c61bd55f21f0beb4ceba0e4fc2b93395d Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Mon, 2 Sep 2024 18:36:34 +0100 Subject: [PATCH 165/188] feat(benchmarks) Add LLM evaluation pipeline for general NLP challenge (#3767) Co-authored-by: jafermarq Co-authored-by: Daniel J. Beutel --- benchmarks/flowertune-llm/README.md | 54 +++---- .../flowertune-llm/_static/flower_llm.jpg | Bin 1627444 -> 0 bytes .../flowertune-llm/_static/flower_llm.png | Bin 0 -> 119787 bytes .../flowertune-llm/evaluation/README.md | 46 ++++++ .../evaluation/general-nlp/README.md | 63 ++++++++ .../evaluation/general-nlp/gen_judgement.py | 130 +++++++++++++++++ .../general-nlp/gen_model_answer.py | 135 ++++++++++++++++++ .../evaluation/general-nlp/requirements.txt | 6 + .../evaluation/general-nlp/show_result.py | 36 +++++ 9 files changed, 446 insertions(+), 24 deletions(-) delete mode 100644 benchmarks/flowertune-llm/_static/flower_llm.jpg create mode 100644 benchmarks/flowertune-llm/_static/flower_llm.png create mode 100644 benchmarks/flowertune-llm/evaluation/README.md create mode 100644 benchmarks/flowertune-llm/evaluation/general-nlp/README.md create mode 100644 benchmarks/flowertune-llm/evaluation/general-nlp/gen_judgement.py create mode 100644 benchmarks/flowertune-llm/evaluation/general-nlp/gen_model_answer.py create mode 100644 benchmarks/flowertune-llm/evaluation/general-nlp/requirements.txt create mode 100644 benchmarks/flowertune-llm/evaluation/general-nlp/show_result.py diff --git a/benchmarks/flowertune-llm/README.md b/benchmarks/flowertune-llm/README.md index 0cb69e7ff9c7..ed2f8821cd88 100644 --- a/benchmarks/flowertune-llm/README.md +++ b/benchmarks/flowertune-llm/README.md @@ -1,4 +1,4 @@ -![](_static/flower_llm.jpg) +![](_static/flower_llm.png) # FlowerTune LLM Leaderboard @@ -9,39 +9,40 @@ Please follow the instructions to run and evaluate the federated LLMs. ## Create a new project -As the first step, please register a Flower account on [Flower website](https://flower.ai/login). -Assuming `flwr` package is already installed on your system (check [here](https://flower.ai/docs/framework/how-to-install-flower.html) for `flwr` installation). -We provide a single-line command to create a new project directory based on your selected challenge: +As the first step, please register for a Flower account on [flower.ai/login](https://flower.ai/login). +Then, create a new Python environment and install Flower. + +> [!TIP] +> We recommend using `pyenv` and the `virtualenv` plugin to create your environment. Other manager such as Conda would likely work too. Check the [documentation](https://flower.ai/docs/framework/how-to-install-flower.html) for alternative ways of installing Flower. ```shell -flwr new --framework=flwrtune --username=your_flower_account +pip install flwr ``` -Then you will see a prompt to ask your project name and the choice of LLM challenges from the set of general NLP, finance, medical and code. -Type your project name and select your preferred challenge, -and then a new project directory will be generated automatically. - -### Structure +On the new environment, create a new Flower project using the `FlowerTune` template. You will be prompted for a name to give to your project, your username, and for your choice of LLM challenge: +```shell +flwr new --framework=FlowerTune +``` -After running `flwr new`, you will see a new directory generated with the following structure: +The `flwr new` command will generate a directory with the following structure: ```bash β”œβ”€β”€ README.md # <- Instructions - β”œβ”€β”€ pyproject.toml # <- Environment dependencies + β”œβ”€β”€ pyproject.toml # <- Environment dependencies and configs └── - β”œβ”€β”€ app.py # <- Flower ClientApp/ServerApp build - β”œβ”€β”€ client.py # <- Flower client constructor - β”œβ”€β”€ server.py # <- Sever-related functions - β”œβ”€β”€ models.py # <- Model build + β”œβ”€β”€ client_app.py # <- Flower ClientApp build β”œβ”€β”€ dataset.py # <- Dataset and tokenizer build - β”œβ”€β”€ conf/config.yaml # <- User configuration - └── conf/static_config.yaml # <- Static configuration + β”œβ”€β”€ models.py # <- Model build + β”œβ”€β”€ server_app.py # <- Flower ServerApp build + └── strategy.py # <- Flower strategy build ``` This can serve as the starting point for you to build up your own federated LLM fine-tuning methods. -Please note that any modification to the content of `conf/static_config.yaml` is strictly prohibited for those who wish to participate in the [LLM Leaderboard](https://flower.ai/benchmarks/llm-leaderboard). -Otherwise, the submission will not be considered. + +> [!IMPORTANT] +> Please note that if you intend to submit your project as an entry to the [LLM Leaderboard](https://flower.ai/benchmarks/llm-leaderboard) modifications to `[tool.flwr.app.config.static]` and `[tool.flwr.federations.local-simulation]` sections in the `pyproject.toml` are not allowed and will invalidate the submission. + ## Run FlowerTune LLM challenges @@ -50,12 +51,17 @@ With a new project directory created, running a baseline challenge can be done b 1. Navigate inside the directory that you just created. -2. Follow the `Environments setup` section of `README.md` in the project directory to install project dependencies. +2. Follow the `Environments setup` section of `README.md` in the project directory to install the project dependencies. 3. Run the challenge as indicated in the `Running the challenge` section in the `README.md`. -## Evaluate pre-trained LLMs +## Evaluate fine-tuned LLMs + +Once the LLM fine-tuning finished, evaluate the performance of your fine-tuned LLM +following the `README.md` in [`evaluation`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation) directory. + -After the LLM fine-tuning finished, evaluate the performance of your pre-trained LLMs -following the `README.md` in `evaluation` directory. +> [!NOTE] +> If you have any questions about running FlowerTune LLM challenges or evaluation, please feel free to make posts at [Flower Discuss](https://discuss.flower.ai) forum, +or join our [Slack channel](https://flower.ai/join-slack/) to ask questions in the `#flowertune-llm-leaderboard` channel. diff --git a/benchmarks/flowertune-llm/_static/flower_llm.jpg b/benchmarks/flowertune-llm/_static/flower_llm.jpg deleted file mode 100644 index 96081d9c2ad1990ae72819f3f5eb69d969ebe971..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1627444 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%0A7{5F5BYxauj^MH64Xh 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

uYG7s(h)Kq}}$!(Op@jR>4UQ@p(|Jln0WvIY;Cu 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%0A{S1D=bu=GUZukc`_7bEiJ^v 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

WByN7-JQ9khI$HWT5(Fyx23#yG>%PvDF2 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 z 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⁡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®bRa+qB-04B@F~}n6 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¬r`37oSlOB;s(Zf!|JB!xr^jbbuI9riTA>nf9uYqAEZC3yt<>yis{oTUylnUa=a846k)1V+p)#+-9;5T+tgg=H(^ zc+@*!%Fj|Fr&W}Cwj0uY3=+WpVTOwj_>$X{Z;WGo!jX@!(-(hFjL0>6@T9`(nWpJp z&JvU9J@Dkmh);XR!IKf*W_9Bh^h?zvwi!;%bDmz5F(amIKkUAJ=(46aeaViM)Y zbxPcmC^Acvg`o*8OaC6(^)w?Oe*&$ zzReiEp_5fUu4fs3-UZ?mYB?Rmrv`NNY=|?>&X`65RHUH z5`e>&X|)3EP=+o1ug0B=^`xxRjwi%}3V5&1yoqrg>qM(-uIY;on6*y~Y^#OugeS;8-)~r7wjg^q-8A{cP@2bs= zOgZHL0632rn3hoG#==;n>4+;NVm*xZzm`cT%C|ktL{er^n?xOYT!DJN!3Z4U>!~Gm#ct$yn@nt8bq`QdUD2^(0Cmv%&y0WG5<1-{ECm`C) ze;*$bDgsB(oM*X^LOir>yV*?SEB6RII!B&VkDX!V^`;0F> z5tn}FA1yTJnaBH%PRQ;NKhlMDC_ucxjBzNk$Ble9Vc&)=wV46^IKuM(Q!a9Ppsz%^0%5_s&jqg=ax^m^K!1+~5 z;(bPm`+6W6*Wp!i-J3R;#*Oyil@wsLCuG@>Fi@&JN5Tq2}t$9QXnG%VeL znNG7u)szad`{Er(L<^WVgE=URoyR`^0KN{2j@{Svy-tLjI7pb8S|e-m`5I9Z*l)Ac z>Qn9b;}I~S=hH@DTSt!`e0SElkqrfybwUnX0Ax=@+$U$%XUeOR0-SRDlyXF!khL!@ z{NFPWnl+fM5(L&uwF?`IzRFUXof}4)zl@osW;B^fVrF$*pcptSIhqnBIti%=smR{S zOKNJ{^0Er6_5jS|2nA#8j#us`VMG+9she(xit(3Ba2ynRH;vj8Z^67WWEoNI#^>y1 zZ1#;uXpRvz;YqD$YGG;w)!y<1(H5wzqNS8{IeA}?1!C;Rc?<~u0CxCB*!{+6$?gcm zj!yG_)IZ~J=#i3C$hy0V5xTgiHpLN*JaTjtt{wa}H(jD*@7w-TrIRM)A+3D&6H42T z&I>BQDbeJ>cCw_{7Ej`CHp>-E>QbL?imMEm{Lz?!H%E(uSh4Z$*FLFcH<}!IFqy;= zq1rbvPWQ@O@=>Y0EqN;VcHLd^D7#P=d2X{&38*$$E6rMJRnf;L)EUa*u-RY>HX`pQ zw^(@Nd#hdl06+f#M2Fz5b5RTWqtVtIQg;!B@6GOz=jtf6r%{TrU2ZoAR+%c1rCvnO zm)_#ja2Z3Hwz8qBT8I}Bpk)XK0tS4CAyNMTO2Zi%%^%D0nvQX@(KIY!b=G98RKLl=AY0+R{;pYtDimo#&@}{Fpaub%{{S|Oi8h+jc$2D> z!}3qd7G|4gUpM*}l}0>>=IR@c3Whj~T4HQp0TqrB5vi?YVllCV*M!1MmfokBs$9A zG&kLOeNHpp^SG%N7DZ}u^zVl>0+N*E>YG+`9ve$h3MXmqcNL1Fpz5C*ORB2KYcipT zdC#@UH9`3b^^yh~W-pwwY-wS~Kg{G^Y=&xUmWyU!c@$WKgG?r8=1zS(R0)Xh~<^G9d8^efWlH{9EjMGDD`VtImhu_d z#r?|reBse!0&l?=-s@I1;`Cue+B7)^3zp?atfwtb zY;45InB!S?hQDmc0v5zjOYW+a&&qjyQZt}*wf3b3>&XgrNMxpFr)QFmgIyFn8 z`8HMUqacjUcfz#>E>_H-QIt*jTKb@jfk{(N(9;g$_~f0zom2i3+rVzE&EhzjGPm=Q zsI!Q<6WJx6*UQHX^VLl*RU@mFrLk`hjM(gr(nW;)rMs>GDD; zX@o)p1`6LEWN27%NAq1({Hb3lP%8G`;?;>#W}FU;vTCe7FQ*b_N?Xah_aZGYI~S;p zsfxJbWG0okA5^3AGg40?=>>5V73CIu&vp`?3L;I3eOR$#Ln;Ztm<3IF?lx=swFyT`7_A!O@N4#G@UBio1TArKK6oTN6jWGAc#<9Gy($82CrP= zympMtfV)m}CQ}p)!A!dMa2v({0H8sJ7=jL`7_pR~@^TBZO48IsuMat*xh2ZF5h^kq zOrEEZefa#!F<%MssQ&;B!&xOvJtDhuH<_MXN{h!O=4kBoXL+1-S7O9uofmJ(hEP%0 z$uW*&9OcWJ*-7olvk>^cGZf-Gp6awsyMq| zb{W_|?}BUjj#?t~ z3Dy)P6IbL&)Z2PBDsjYPH8Uy7nj^TCU)>O!aAIO%f#_lk(dC5LHeZd)Qot}IWgJVP`E$|)K4V~!|h;^ed4q$3#l0Kq@^i^y0Sh# z?pJAr9|s{)rg=<(YF#FgQqZE%HFxXeZ(Ztkk~<@7q^MlDE!joUfeP#x&n&qXL}LlJ zWJRKZ!K?!XO^ifU3FXSlVpSMJma`(UWRB>n+9rDzpsvRnjYyHh`$14GE61sKE3eD! zi1y?tjZlVUK_C@C)m!*RW2+h_abk1MLo$he;x;;hA!yG9!OWTNkI9<;<|fakQCv}t zPf}yDsQ&Dipv(71ew{AbC2 zm6j^Eky|o^>n2O?B5I~#MzKeEgF@1*^H!2C`I*Nb(?**GBOX3HVn=A{Rt-41$%!PO zU6Fm&3CKC>3$5e5i{moOR2JF|PcYmg`ody0)gkns;@W z{CoN_1;WiZI*7ea{Ou7uoJ?p;so`{0(#ddjx;%=ZN!ECKavH)&%A2y7EXLNsNz$cA zqB#MllvDoz81QukabqN5Iz2>rlBe-A+=73l!?eYq!p4?~CK^K|ELEhc%r0Aqh}MmO zi8^z)y7uD{d(N*{D=@jMf8Bfg zReLEjiJ*KkvVa)As@nAl>dC3aTyZ%Gp7iLW%G_vyTas!*jx$aP-)Ff$6BCCTIV5-d zW(N}Mt!b#TnW*o*J7y+qIPT3AqtvI#MF>qXBRQ@>L&RktD(VAhi>cjnn0WF?#;54Q za4mlGZNgiNKHfu`%VxWiGo~_Uf|Pq%$Z0C-BlfDK~;(t@;IGkvts8UM;phalylT2Skz?YUe>-txoS0ZY5T(<9HJUg zlP305)^@GatlZQ~v=K>@8_5e>PNCHm(SkJCRUtK+0<$NKujKfi>J%^oJANkqxIm1U zL+QLSp+uWDwYO)&k=;c}C#B!cu;rFrCy?Ydk~8a-lp{3?y^i?nV$n6PQ{h+AxfU`i ziuDb8CFJbVo{5{GjLj%NZcEF+1C|U`G-5zpV>A4|T*nz|V5WSy^tW%4^}hZ<5~gBG zi1Y$6<;m2`^l{^YCsvZRuRR$P=Qo#GH~w!#EK!oU@>E{2ZzW0C+^+ZK z%JFN3s@c!0oW>^~)RJw^+tX2|c@*ntgS#Es){!EBVkS8XwJT*!nh427pIcUpm7)bz za?LrGaF_*Ly5JA2`^hCSta;L-O2*EpzBjn8DN4J!m#c_mip@PteNo19D7M#momQjj zF2t>U#N$Rs=N_%#OXmw2ni_35BBU4rN?wdf0%7({Y!|D`W%ybnD=s0yX=9zI%eWn> zh|A$8L053$%<=4LqH;kdW@p?atEDqGhb%-MJ(7vqUpMU*+@Dz(F{MMN9a8N}Ilkem zB27OUq}a_vr)S6YZ5i9dW%-OrQ5azK=7Y1Z6Zoj1y0ifV?enNmgcA=QP$wH%BOZ*A zx#l3pd1yo~)aT@NTFyz>?JexBK|2~)=0c0wGa}VhR^5w2Gdinm%;}xkS(3k(r$);m zI8c|iCz`cHd}za)ZR;%~i15%>abO)Y^!V!W;}x2OQH!#x$%CW9CbLY9*I1 z6=)_ziq@7-+&?mFIs%p_I=Rm111l5^j#g}HQp1L(<8{-SvQJ{rWcW-LZT|pe#bkw@ zWXX{P;FlfY-Vf7`pGs1P7q+RZK(u~TDq_$mR!GTKTBU%q$S8hECmMlrqA#FO%7!qv3^jq+0vCO)A`1qyM(c_Sj~WXCbbQ4?wJ5ynI-#-qJs z&te@mrj*XN9cxRzbr;{dlbpnCoQLC=c?We?*p*Rcck$UyV3?ktx5r+L*7P+Ne4$q_ z!|$$D;-J~{Hu6$2%*+ zS7_XK=gK;*Y>!Q-$t&&KMi0D+*(>6^RZmK+yMelcyM<0J12HaXu*?{%DUEZ*f`{uRtMqw6~w!Mr7FwS zAhP6?h;T5Z8aRCm9mnY(b&@KvjA+erPSnNpZr}IHcHcEIC!@2A)Qn*~d%{*CdET5) zZv0lH)xqDz41ZU&0(vca_@vg77gRKT*jXKT;Hs+IEJUsth3mpmopoN;Vw^FJPBiK@j(SA{4C<*HTca%If7 zpZSN5PvcNj_Ouya3vRSZ6wOSDiJ1XXES-l&cX^;d-A^#WG2{??7SuEwJ2N>Oc=>-- z990LWv^DWM5n?uh@m(iq10IW)Z->7~%@bW?F{p@tJ9e>zA~-a4MkXrw)+AffhZ$Wc z@4K|-DMTv2JCaqn%~dK(wD4CS+f`t@minoY?opV`aO2hD0Is48M<^m@M;Wx(>bm8? z`g->{F=WY#Bl?}bX435h^=6U9UPog<4#d{6>2Ok%bx@Q6F5N^@wJ7OwLmwc38ifL6 z;4x=L!DbASy~no3V0 zj@{0L@LM*V8?h>B*GFX9l$&HTu%9hS2dP=O%pb6PYMN}yjL&&S&MpRj5(D_Zc$MN% z{9wlua_ea&qCLZt?j9Xw_VLDb0tA~{!qO$1!-P92pNc|~W;0rpGNjQVR#wwhnVT&* z@|_lRc=AIjsbUK*qho=g!lbNuGMt@Gr79oYRL+&3gx&}+!EU9Pnfj9#xXGSWIO*og zS)J>|q%NWGXO&D&jO%1aoadvt8&I=~$IXP1t812XwP+{eHG?Pw1rwHo`Y>34c-Oar z3HIu@TvGyTu6#=5NJ=syw<%UqnTBvz+(1*4CIvdupsb&?mD~GHQnec?RIPec%{dJ< zRcrCM%;53knBEHF`3uGv%@Ssbh71{IpYOKOvI3b?e{fQTmTX{|o^D_kZpl-U9?D$i zl=N#H;Rh^bSaMot;>GghcNWzB_YgJJ?MaBMYohdup3%Cv(oFA-?%j&(s~I&U*-*i= zyQ4a;{{U4M5}*~0>sPpqp>r2BT&6V_LVp64lEo9+ZaubB5RF2K2Iab_=2(GcYl{nVvtc`;BT6nKALPGF1(V<2Fx1 z(FDIC%Aw=;3FVYyT|T9jplQKzG)0|6e}5@lAW*3o z0%&#%O$b#)nJXRgMmb1xiUK$ZX*U)xEXNHwi4qM30BlX--u})SjHZnOm@a+hPC4{^v=6Y-jd?d)T&#T&SjBS49!gyk*Nu? zO zre*!PQRJX@ebj4>=S|S~{j|cAo$ix{iVo0Y4Yf*HsxMkKN$Fmu&x)yU^tLF>a%abh z$0G`CAq$~^{oCAHBlE)>NTIT z^_JFLgEU3#->sw;ipxN;O@{vf8x{p$3g6Yy7!`e{T+xRl%=hgcHC(Mba2INAYr(Tk z2_D=8N?k6^WlMPMc~&l_@_|~vAB1->%&g*wdW|6)2}YENp1&HM)>ULfHf9Z!>s2x+ z+iX~)trZcI9i|pWV=Qa(TH8F1R~4@r2sBM<0EoWx^s%Rh7D<#UUx5_r8uH!e!?$T3 zJaz_%oO7&Nv#j^XNVsG(T~dnMqs{GVl`N6*S5*H1$iX<{++~@Jla-i}R>7E_N@z=m z*wXO|ZN(kKC>}gg@(9U{PH~cH}@l7sZ1v2F`)`az+D@V#qYB3c?jhlC}G%W0BBuw;kfJ$=(Xd5tANcsF$bs zk;=c)so>D`nx!n)SD;>OhPdQI4;AQ&@CXLaIh7}5T%w3sl}V8 z8_0Bf^uZ&F-KEsoPEXr3B(R`0)F}BsHXiR1Q68v$G@e5Ye12H)BsOr7ndF zE<=w<&$uO37UR2SOpp4?=^CJb?u=ht5!H-c@?^1yR*rKnbmFlSz9>W;$z;gyEzz9E zE-~7*EP5$f&W+bZgBE$Ane7LZXbC`aEHc5L6k$p{u=MemrIl?0ZRU{2UIet9n2n{_ z@dTs@=c>Yu0>YMZ5jBoZoC<-7K#3)O5h#$FD@7P9EZ4N9P$wQvpwYsqn4E6(*HvOGTs%C<vx zmSBv&Z9)&`cPWBYYOgf^0GLcEZgVM&C#jpw#MQKs?FIw`Fk(`)O@ekPDZNFOT!DV+ z-)An-*+8Cs`}l>fl&3sbWXcSy6`IVwU_%U(%Vaj2BrVQpXYP^GL5XV#A0t^!Fik8EMYEtL5aHDS5R) z3#y2+oqFlec6q3svl%F6_yGbezp_7X(J7mpUAi+V)+}sMs$J=D`)MvIO$4x^GcZrN z=`3ABlB~j$BU7T!8H>A*qo4LU>k~f-F+zdbfU=7i60KCzX%$N zkCzP02PQJsJle^RnHhSnER7W;(-JaCD(X(j6dgx#6{07WT5Da1O@ z!O6wxQ@El^hbDqcR;M8t$u_T95n1)G9h);%)d&a*PgF(^g&4_)9F>n};Zh}TI?%rp zu5&3b%pM9RJr{INQ;|$#n9}1Fho!X~J56`;(}gj0+7&E&k7#Mhs=QQ;rbo@M zXqo`Ep4$M^M%*S{b!5T9Z`~Z5PF1fcb5y;>li&|%eV60PvSu(cVUl{7VI>Y$_b{Wo zSCd}UndQ{IhZD5DG@WJELGliZDp47s*T;G%BEc9@LL;a}&V07jlSHu(Ih?r;ImsQ( zomrW)fIOubf_v^D{1lC2RUwRdXC-D;t=bs5S+s@K?E6dIX<7kJucyJKI@1jTR@0!U zwC7l=8P1o|l`AKr7C{WEf`gVa{QIfG#p5*olWBz7c%Gfq$g9z=|NsmNHRP`r8Lsn zUgh{(i2{vzs99AW_MvhWgR|VVH3VQq)P>t`tjjJuz`pCk_apuZiOlBqs zYF!|d^c5!U$7Fx0aTCi*sW{$AG21psP{yAM5gs0Gd3;e#X=S5lQC6eLEkcTCLXes@ zm~rf_#urKzAzu%WN{prl&yKf~cveoHKB927X)zRDVv2ckB`t!=iM&=AN19;!d}|ST z5?t>?Gg-YBIfk{NQvq^DV7QeRMSpOi*aj`WTA(zL(<-{8pdEXhw%Y76{8Zaqb^T4_ zQIDw+o3rFoR6$t8Oe|`kh%kJrT;eN*%G!iFmW*+ATX)4Ulba-XN<0vSyX4$_K&VTBRc{{!>vFQ! z$N?Q^n;FTQrp?*@tD5UVIk>f>DVVI>6V2@zkj2!1G0Bsc2u{RuHHq1^fYM2F-zUes zB;jF~tIZHqno_c|y7AJEi^wbsNFF#UHrwS;asL327j~ejmZfmU3N(C_Gfp+P-E5l! z^?xfrMTAC0*Om0SJ&4lSOl=#fbWW?dw$}SlPIC5X@zQ-%RavcQq30?k{B$S5egR{V zs+DIS>tpEs#A&R2%w*?Fb2^v0#ioeY>T100dgyOnHICUQ8JsbWVL^yCx76nmD@Z*K zQ{H~a)}iW?tA*l+6q8kBI)o6f8V>i&)>10xvD-k!jvKju;iBJIVOa9wwj~_;VamEy zgS@g(8q!9bOd|`EpG!qo9Ok;5*0T|$WtlskqCbkQ>UQQ#_PjJ*YbY}GIMwGls)kUL zY1X(?CKMgL z-D7r)fX@xmhfh~Jp4%VQm{7R8MdGt@7|_S5)?Av)Qt$BBYTa;kBX?0T3vsHMMCe^! z(1k$DK)s^@COQmxi;DhDCyHj$F8Ksvk{ZH`?TKn2Us z3eG}>>R^aA6&Q|gh)7*HbCj5s(L$UB7}1q(CbqI3QK5+$KBB`o=L-A|zLwF37jGJ? zoqjjv7E&2&sVMtNc^$D7HH+qaSBj$eC@9P0kL#K+2!UC?mf&Mba^{>_)ww91c3yEc zO`#5Jq)cuk#}aa}BBfv3&P7%62}18@3JT!y@zPbB{8=wcHqT0$%%xOQCLB{k+Ko^O z)fnQ(|Wvra8yTb(Pj z!*U{dA0dwd&~os>OAryV{F2CrivTtG9P0m6^-(C=q|DKOd1*(G+m0tmalk zn>m9I4+TpBQX5cZDZLFFldzn`T=dRMrI5*Xh^{@>O^Jr02tPO6F7h$Y!1gh<_nKm? zv_{&!tnBmck&!6ec>rdtM)?G{%9nql&mcioJuqbXDpenzTvd+L$BygE3!bW6!gCO^ zM^hY%)XM}Jy|>`sD@2}0b+-s56=i0-&QT%2ECQnOXc^s7%CC(|zO4BTsM(u+SUUZ_ z9FvnDOu@G`ODXu2gRJr$k+Iz6j=)4^UYZmWjm@)U$2l?u2fd=F&LKpdT$!=iujSKl zZ&}dU-4bp_&Xx~GPD$+K8W~PSO!i{oaoLrB4>mtca+KjOCv26nC&(LA8%h4bKwJL+ zIi8x;PNDmF#k`o8dmY8hRT_z%snoE)<|tdC#6NM|Cq$yOxa&Gmj#&AIdSKeJA`Gac z(=sElg)g`V3~wGYhB4z<6gt|$D$x9kla|haX)afqe^MmkpKP2!n2dreXbLhZit?;Y zkc)hzeABE#^cD#k6juk4WvO7(5}@QTFCW}>fuz`0i?&^SitChSL;8<+;QdA2Q*vY( z)DUr!=lAfDDC;A&h)nj=)WwtN^@bP_Y^BXnyM?((>OeE#IbP4bpt7c*0&PpqmmVFr3*k zO3#?qBnYzHR>hItBxFpclS^_nSrMO;CGJU?hEWMgL8}EL^POmq7A&lEeRR-kiNE5Yp5^3QahiA5H^%AI@%7Njna=r>l0o?mW)}QN{=c{sWptc%(`}Z9ZCVB zjD;@BnmxmfW+0BK;a<=3lU~p#`z2}M=kqmbc@DC3&N4<5Slnxx(dsp>@xZ`{TZVr+ zcZ?VfXBpFKydgR(j#3fb6|Eh%H9JESV)GiE>mj#CGKjcHKpdd)>QsnrYnzf|N$&63IBGOL2;b^+d)txw{=n(dSg>$_R%}>w}dDV zF~wwyM(1CT!I(trAcZd^t*0QfNm&h?TSmh23znrCl~yULg%k1FlSVA~+s%)UP-OVX z#7tA>6bDEUOfcFnTsO(v(-qEBRTWb_DKrJQ*%A?rh>L1%NNn(#s)8(nSS-s?-0fn7 z=e3r0akd#<3WR5<1$g;T!04e-ktv*!kAR=2^9w>yG3QKVTDq;HXU08Sa!y=OQ^rpy zHPez~j7nJ_AvU4YS4$3p7SfybRxX60a`gk{MGi2H5p=Nal)KrEUNV-Vwjx~4|85js7bJR z)JG~488aArGuv|udOe7kdWenlM8;b!BqC#{GaHV-JrfB>SYmYkElNCX#nwcW=;`%vX)pqvNT0L3KrKR9hMin+FeL+s&3thZ?5PneE+K z*4Hs##LP(C_pT(Dc!FM&$0COGMb&hIjJSr$KMIbjW%adW@L_yXk0RTq9Ikrl1MmX>A;aq0}eO2 ztW?OaJH*;&9~gV23bIaYYNdvYji!pNR7#5GiK*DTP(Y{5*;x~>DY@mz9XetKADacs zn$Y56r^{*?iutH?s+dwoAGT{VYh}kasr|W#?4YR1dBPm`Em$sdP@_T_X$ntc3uLV6 z*!I;({{XjmI;wnR7T#Jiotcv%i(%?nE7X1AEJsJa^zlu%-^9>~EkuxV@?-jvawsGf zSG`n5%b%p9C{oXd3d)_ITM3dq5DZqL1CsS^XM6{hiz{Pm7`o(!Upb7ZM6Q{!;P;ak zgHz;a7ir>*8`&)mv5*X!D)GNHNi&low3|z*HQghRZc>T7NCr=J-;t3K)@%Nu{{XNa zzYl8tiT!B(zo$Eg?(ed`;`ILjVSB6G-jKJCsro;odY7$V++S`z$NS}`IDB78_xwFe zkH)ec&nw>%9!5l`&_C=K_0jG9*Xp0cd3a;@(frO4Eb+(yIbxPbWXX@5otUS$hDo}W zHCa1C)?)ZsX^`GfA>{{uPw7^?tON_{RsZ3{SWn_`bGN@`jqrT z#hw1;dlS>SzUuVe9!DS0e&KpE+uOf!eZ3TSXB=J^y}c*bxcqi}HMr1?^#ZH)^bS!t zvN1m3*5&eNR)@UGLGi_<{Sk_HX>Q{;WS=KYM-4_S4=!Z@uyB zA9p=#>>sCkHz(D(UhVXrXWgH6@p;~zPh9lgZ=l1C=!4Kc+y4OL9^GLsMW|FPgB(To~NnxJx^2WdY-4$^*v9i>Uy73)b&26sp@@CQ`GvNr>XTl zPgCl8o~P9HJ#mTtU4Kd5vHH{f8hVG=e?XTh{af6Bd;b7UpZ7dDaw&gC^d4{DKVti9 z-#+Kk4oB^MBiJ6{_gAX&8Rv9PH>26sMv9J(!IClLwESs*@On;h)W=`-Km0N4k{@pS zf6;xV?f(Em_OG`6JB{f6-S*F;dS|!&2huo!mx<|qi|E`adT*urcNJNQUOYHFZak0@ zNLhiC^qKEFpPx;Lh=_>zk6vr)dcRZZdY-y+{{Yp)?O#Z5XMTvi_^-p``;XK8(f703 zs8#y?Pn+o;r|Exgy=Qd+GkzZro#@MkEIMiHUJLo%myq@o-z)IGE5oc{n> z^v_%MJ|7}}iS7RYQ`c`(|NXJfR^6mcs>w50q-}tZE z940;CU2`9VTHpK8ss8{JzwdhZZ__{Ocl{S~y>E@f^&hgosc%a3OY;mX>qpT(+VgM2 z;=3}(oyqi$KisUc1J>4@ZCWB1=^TedoeF_GW|YOMEiWi{_2nZ zCjPnI{V0B?zQF#m{@%mxzt|tRo{jJS0BKKE!)7*X_sYkKW&? zUvcp(#`VwC54;v0gKC35Wj^2iynj~o9tSIrc75XfuXlQvydIM}e&QS+A0T&r53eTw z0Bv8bztlg7{g3Fr)kaJ|qvBmtJ`0<13JIkD_uLzjjLzjn3LZP_9rLveqdX?_Wm9PgxLC+?WeQM>-N0|$ z2(}a-pNi1_R&4>mAC32!%V#X*%WU|@LE97?5Enbx$xE~<6BGG`d}O32teF=5%zJvs zbEPmj&7|L%-s>h##6-DIj0~u(XG>0`IOD5Qs34QQ7quO%6-B40R$WNc^)`%EtXq~A zD9MV&FK-svtvRP!4l-+c%kMEikJt~n#$jc4P&U#K!{IRHj+~dsP2Svi+qlO(YREU7 zMy&fhiPFJQX~`^MME%B8m|ri0EG?5;9{r-ae#|&fG976;|@blbNs@-<7^#m}lN- z+Kh2+^<+J9h(k_byDsT!8U3!N5>1_JBJmd$fM&Bcv4W;z@YqHz&BCQ!m8;UF)qz(| zIpkE;-Y|Tk_a$n~2E=t4&;5+pY@T}!{-!mzgyDtK$2hduA;E+e{{U&_ug$~qh=R^E z$;UZo9cEz}jpQ@h(rE~j9GkP}km*%(V4EN0@{V*+)GWZ#b;)B@ zRX_1oqLe5nB+fFu#*EZd-o5RHL<^qMCP9L7*H*Tgq2NhMQhE!pO0%;Gy*L7`ubmUZ z$R!I+;R>`G3Y12f3X3rOkZ4(458DPPi8qxPgi9|{Xb{7vP3tB6v>kCQHy_)=#x)3_-dNR7J6V>rY-A~h&Y&x8IB%q)r zV2Fjrli8%;sZ-u!5noe*{dLp-ofa)>vJij2CwC!B1 z#zNw4ub1K?YI*8R9cjLKPAL(}3UfUaoC{gnNXt1@AAXmYRYxV96_8#$%LNFT(?hz7 zy78}Xk0E?Td+~SHFq33toji(m+j*a*{PlmW(p~46_SYqb`w&qcrSjImtn?Yv3BFk zQ%0lANJ-jm^gSpA6x|2T0V|?}V9krH!;wDXJ-f|g<+*5b;Hzv+Z>Us?&cxc$1uvzI z!w=NB!bkZDC}o9PPap&$cm1YRxKin5~r|&?+P0NK=PjwQJ1Y?XRm6tTEfsKm7N125J3*c0`M4fR+#$zf_{mh5+ zX-fNvnBw@(!fG`lW4&v35ymY<4Irk#2EpknmZYS-l6F}Jb!J9Vjt=kQrL{P20hrZs z#MTLmuF$wg_p3w))vsx^xT~1*V=}as6i)29*?inz>`shIb<4Rtn1>-XS{v5%?3g=p ziUp%7MCpyVsafD{1*0L*xmmLYS`-8&W5EcOGwxO>CNjjEPPoj>9qKd*WRBWdPpfXiXwS!@(9XT+?rE~MIGa@v)EUsuxx%if zC>Kp=p&I8Lbn+a9LDAkRW9IuT&G%{{r0w+l`wbvWV`*Eb)cA)&O|4-O^O6+D%7`i2 z`+{N=rEXAB=h`Y&C=&)Q6-8n&Ng$asYzQPNfB8+7Sfs(m;@JjB0YaxZ5>FQUq^vau zK<Y(e>PbCU0Cz0Aig zI}(fvu-ukpP(F1X%;U!^Aw5Zs_k(PgiRn~U2#Z))#WAhrLJuIED#W8mq)hBq`EKns zp6f?ip(cA|wPs;lS}sRhAmj0lsMG{JIZS~eZ=+L#H#quGk=SGn5whMA5pFNIW=kbz13Q7 zSokd+)p1gj6nQf?3i2C)8$R z7Bum~*ArJ3zvLnk0l?r(yAaomOQ>2-$^`!aafU>#(GjC<2{4^InZ7%M*Oi@1XpA!9 zbu9Ih4q2b48^~a_%-tz#6kZ`3P~qhYBSS4>qp$+yoR8VL$D4&>WP%~YXyVd9{8}SU zIRaa}9U&;T;*2BKlmcxn8nQznH!CNQ7$EW)@W7qByi>K<)_KnxkqG43dXtd2C|b$~ zj=(2w58?F{0H^^cG43Vhr+A+e&KDFNdZ@&WtKV9Q(SUMEc@&x+J5tYS^$6OPWaMgj z5)-L+3k1`#g6xh3zpiv<4Ean+v_=Wv3J|6x$DQ<{UP!F+CL^^)@6Np)rrOR{_q;xb5{SmN7dr?9)_zUyWp0 z)#YXF+DIr~aQL*U7M9+McP5q9zm%%0eJVs^#S+Zl9Mx7a@vykm{{U2uL$QOhd&N(t zc_xv|@Pi69C)JwTXh6J~uBd24CgSzP-}2i*h}cBkIFkqi4&!UapWV zbERyC64BF+V#wq3shUukA}r)po?;frZXgpj3D6AevvRK(S*XgSzNv(E=9x>jPXG*!4%KO|Oa%9zRr zVGW&pS&#Qp{to$5Ig=S9r;ylcxS(=Ye6C4{+;Xpr3w+6Bg+I&E#0hyO@fLeYLMM|z zmYd2$tqtcT%K^4w04g2nsq{UlYykvQmx+pO{AyUxrs4>lM~ z{_I|{XC;AYv_S-bRy*Z%+-P9A_Zc!{pXIVxkmG74Ro7UfaWOFJDX$|cc1oH`4t1pU zdD#NU({i`H*6iM>%&D64nw)tBF0fc-l5Yh@W$&T~Ml)M5{JEP`DUE?+v*4YD5w5Iz ziN+;~GYU0l!FGwLKyCUPQ+;mqEcBh?12{nS&%jS4UB5 z`8_6+pCG*5I@*lts=DHH>tY9+zk*>XpfH$)Jp+agEqzpE9@8-?Gb#*Z(u2aLB4q^d zh}-s86GfgaD5&~rs!{`kFk+{+>%HB^oJ?6S*H&T3nPam$?FpzaOZ+DxXXQ>sls2JN z&mA<;^!ljCGi1jdLz@|$_tue?Z)g(xV%+KVUZWzd`7c@G3inp&#l6j8MLB8&W3A_A z`!%FSPBK%LVYI^X6DstQ8gew7nhCYIvxI_j(~{$5WmmW4D$d(FD}Ux#u`Qx8{{T-K zzY9I&Qwk{cJ;;k7wW++`Zq4r`+DYm{grsf6Z_kUhgM4F>7M8Clt_H$+ZAR*eeQ zno(9?jS{O%rBtwEE=5Z&S&UViP5uw*=Z5>M7)n&*`l0zSR46BLj1zMe;7R7B1`#U_ zPexJUvpRIkUHsI|W&wyoWMx9{h;B(mV5vfmHJ>nYfxAr*Ag^sTV&dY4s)T~db=$zK z-!b6C2yhjL$tZoNx7`j0j(1jte^Eq=(eYqs-N3rC))bj1&)7VysJ?3(H zi*OT>$fp(AWN0T;W3urHdC1BBOPA?jgar)xeBqhVzUL&Nj}@Xap*%Ehr^wtZPv>V< zLI!EZq|!YuF+0;14%uV8Y1z6ByXs(hPU$!JQ`#l^N*bG$ti*X6+g`$m>cX41iP@ZY zHrkY&)fKYtjO;5QiNQmeWcK!sBaQc6VtujFaYicI=|^NhsgF-{)2uA3CM}xGSCm23 z+9yzqMC*fdkqSpp*qs@S_EI&^CU03FX)P-4R9PZEDPW9tUDpoITd|ObGQ|`ODm~4L zn5j>J<;ll!IkA%F1r>OaWQravp5ft~hD^nLpKZGyW(;WcDom_%Fl1gJ#LST5dRH&< z@<$$fUT8jg^vd@3bSYh}UsD ztoN(!F}=azF$!6;ry;F2dOKlkOPeghiY*uMkj#_|@VlSoR2&fN$Xrf5GKVPR5607J zSGE5DyqhWD(BMW*Bh8%2#&P60WSbw`-2VXGV$h~J5|NLXQ*3$?d2}PNemV*R$jeE9 z5ekU7Ux4l8fCc%Q<+KS|CGASX55Mh!*p{_zc%Y4~xt5LHH>~?{2x3Q5CbJ{IZKz9( z@yQ^}UsEIY>OBf7lIBPRix1)|Wb-RwLckobIRp`?Ql&Tl0L@IZyo)2vII?53>A3G( zCt8)gZj)G*1YURf6e2AO$(p||enzwL5M@c7+m_<Ia zH|(a<{m0E`(qzm*5(4W+{^%ykbdn%rnUxFFR4@t$~3QFg{`7@@YNgXeJd@Rw(O zE6;N_a;;^(N`uA}6PaYlR-=YQqvg)~a#0rKCMJF(=Tk8v;y*cH%*sjR8U?ioP_@g@ zelVb*Rt3T;AMSt@#yRSvjY27R5F=;g$%<~V6_ao1_J1Zh1jOGVT9XkSfR`{g%0Kzz z*z9+!2vUr#JzWW8R2qs=9JS}F2MiFRX98uwPzC|+bzP6uH<)8HnH!kpqCvLJB%Jce1^ZY#$8D_h2| z*orl~eO^nI)Y%48Thz$hUw5Xbhvz+_4-@g_5@ZX`Xc;Q(?u=;zUez<~q*27CrI<}y zU_rv4oQ!=OOyheHF+zhii6&vrO9qPMY38F!K6tJR&_o<-B;?0wCwzGG;Y5NP zX!xG~v(*lTNTesBVt~}qjd=!XM%6%4;e!b&`C*J|#DT_)R}9fI;yD{*tYT~LHkt3+ zwDRXzvC%am7DBC`9G$JjYlzPnwEqBY?JfC!a}(*}>`GCXyw|(6iD0gTrK43!h9g-h zRs#+Em0guUs54^`j=6E2Lhr(FBC{r=b7c^Nm{BSvMNCOfIR!#eUmu<(Cc0bfcjX%2 z)7_;+YIhonEWuA*P=swF>cPYfg%W6ox8;Vwt03-A?eM=xjfj%6WjP^6^wdP1tMXF~ zZl))2fHmO}I+$IO$8<`0IL6dWYBjjqWnHizy32RBr_2$txWkZ{YVm_M&#CM*e1F`2SwoRznaa#2e{9+(k(u#`pR@|k zc^LHhT;_~p$cjF4k1f{}D0NvT<(Z1J$SELaDxVpD<SwH(>Ix{s&zZ64CUC7jmo4%*IQfTGr5?Hd3M}u zX13JM)_T?x7s-+WK%%apk~Wwq6*Ce<<^pO0l?(FuM<~OddXa%dX?>2SJ62a^%|e~- zT-3gHF>r8{HH`5>AWvzStm#eP!V}+@8Mglb2#N<@OPTi?p%^~qhCgarDK;iW+`yRAoJAEo?cl%Qlur%0^nz9@>y~zu$wqES3ZhSnA_P*?Zi$9r z@v5oWh|bQ4XC^n997ZXq0TPXp`l2;$B=sL! zO;*RXiB&nsn=lQ!V87BFD_%;+0%x|8@k$SkYfVpXL`FhpWHAu^%Z!bvB~K`b z@#&cDv)z!?BNmwS#t7T>5=S0;BC;>~YOJRbbv=|CvwZ&mR%Xg>x*@EYWOZ}nBl(Hv zkoPZ(lpG`xJ|+yt^iMJ(94_M?q(l{#JfX?mox3SeYZ3FO9lI6_mp4@8DK#Q<2~eDM zQ01#5g2?JJV9Behlqr^;G&`;ka&SuUbcQQqjl?HM^IZ!)(NST&8*!<2fX za%ah8$wy;X>vHC%B26Pu9Im7i$5H)u4h^V%HII(;(WGy_k+IgNoGF8}brj#XDp-Yg?FC0@nqy;W5UNjI7?xdr9YT;*kCU$n z$t$Lmtk&l;ZIK9ysgjZ`M+usl>hgj@i6U^bS$~Y-A$2FQ2|hNe ztPFz%OEZ%UMIaM1aWU}~?zFF^cwhMwI3GW=emx{bPSIB z5a)G>aDG!!DWZU*k=V@1M+~iMqL!l)$d;2g$x!`_`Z)xZo6qrCB;`qGDW3_hNrpl_fT%B*zt5RGQJzOVM1I z2Q9Hs3W{TWv#Rpu+O20cY;UZfR`9A~q}JEORqFfjw1kR6rbUv;UF0Qg5WC1HjZu$? z-FGn5?k7;JlF5TKlCx03gX-?;XroHe=Iqb~y2v3}iLpig#D>a{i&?*yDDRqqbqwF8 zzW0WWi#kJd=&<9H2KK`naD+LJ7t8j;U+QGWsWZSv^pzH35szr&kW{1#JgBHpyi#f$ z5EKmZVAM5`o~7VaVx%qfA>*`(LdbsCK$ z86?1k%Y?zXh~uRqB4bld`!3-}H#=2vyG>S0i6e4(9A#RgB{=ES#Apar#>^Op+}>UH8SGr5mbRtMp)s2O-dd5%6l^dvINbIQ;!74a-gE(*0jiO(J5qI zCZ$EYf4n{Hci)tk6WM8ZsYIn}&&HHwX84Q5tF{f+MF}V*NDgGvQn($3TsOj~YnT|| z*-~++$td{Q*e9uv0XF90L_yJxRyyc0=gGTdWm?f$qhGrF)W(v9;dLG<&Vz1{6}OqC zR%(&mOBL*4W(&q@an7MYK$$}O$g?xI)pSN-j$q@`j@GX%dmLB6+2UGhR7F`zl^j7J zvQAF1)=w!>7KPa3=cl@8`3!c>{STT>*v;e0xwhc^)FlfAw2!xCY z>d%#;WjI}4t#a7c^Ve`pSU54ol)}iA%=zC@TWVCE=24yYCeVh?)PX4UQXwE_C4E&^ zk)UKZa=MalFx6HhWkAM0aHIl=DCEtPCs}a=MME48xhOHh{Hr=eZ_OtRNMVRR;bg~= zs528pdCVX5zioHaakpa=Q!QRWMqb{NL&zZ~$pf$|m{E6s!ozPB3xX@VHHww`$dZh? zF$G+lLb8v?SI4RH(o*LueIF+p^?^WvuePyzbs41lc}20VesG}174R|S|Ul4&xS!Nc>MaO zm^HUDr*YaL_Y0ND@`IZzwczjMPn3ab=SojAS`sbk(H6 zq)ftJk-hgiG$+RL1MEFwD_-e|nFFZ6VvHJdEi$Vno=g=7i7XfpzD0?Drsi_7M^hbO z{P>Aq8EI+p@BS{-o3H{&iZ5nF3;xOQHZ`=n(yHffgv zE0>>Q9G9JfT~6v6Yooj8D&_H=g8CU$V=!#;8bm-fP+qc`??*kN@IE(U2n(uf1HzOO zevf^@Q#9rgNJRGr%ABsN9$G$-&Y-@I;PEERtcXo#BPsPUh#JuKd@x`cmVvU9T%(pe zb2$|yMrc8o9J^Ugl&#xNzmCyw$wap-QxHrpk&>-4rJdd;Vp7`Qk~)=1*{f3*8y-6$ z5#%G*ONUB7a=3VJA`5DAB^Q}0g@a5JrZS`>+lxvvG5eTOtWi@N+em{NvF~)hDdg;v z1oGop*SISl1dFuxW?{}@=5fT@xV)rXJCe$vE{#jwY>>O^zA>k~Yh?2@Cu%jFFp z9>A(wXdyYPL?ulU>VRQ7quYgyE7uvza^PC#DUTZk%bL zge4Tx7BDEpW`(+jmXUpdS42@6nafvk4od|En3_`~@-aV3wmHGObaG@V2SHm1O3gbt ziym_rje1pl@ssKdi0>R2oQG9rdnZ-Ss(Wo=w|K{nC*~9bGOJeABeo8NxidO)3j!Kc zYc2^asBK zF+42^PnDMZvR-~Bi_o*N49@ClAXvm6B+^EDy?WKEO)4I0C_vTcxMdFb1CtV&$Cwor zg;f_(S=c7@n8bhvDgBC|D$b7PzVRvT4#%u>6c6lRe{oy2*k#3QT$;P{#8y_xn3)T{j zLys2RMSz3zb|zC!z(km<@8rH}1+h@nStGMMqZwxU=F{W_01#{r1I>6jV^ro2=yaz5f3pW#^><9j1ykXiJcz>FZ2774DF{{ z=`m5qSY}k(lVUcHVSOswwKdcaRC(8Da$kwVl8AwJlDXgGT5&8;J)ol!HJYi zOu$uxvp+JR*+->1Xfa0=Wv&zw9_h0pp3GEqO|q69{?i7SGCl4O|4 z6|WA{-47Kflt!W^W9LTZ9fap|$Q0zW5lvgl%|j~2a-vcA^H!4;C5s0Dol=9ypapd= zZ-#nsa#FUa6=mp1@|yP=l>mWjOck?OUQDJS*A`53n;ae_`Anc^@!!pdD<|Sjij~p` zj?|xI-k-RWfyk>PZpT_Tes-^7ag~)=u?x9vM&&s&RZ z&N2yB$LYs35wtpl&OJx^gJ(LZoyfa}j+B$FS$doP->yQAf(2}W^r#FDC#(vFNq2-Z zUck<2u^-rn=bW=SJ;o_J%@Vl8r=n6?w5VKVq@%#3Yvfg&Q*U16J1CQ0(C<5lzY|s~ji#wqMkvjriiJpt z*NrfjIWt5pjh6z2i-skUlQvwo#keOSKQ)cGSBn@bD6&7aWH%l^daIEvWOXEDnY6`u z5K(ISs*dRvRarADwn1LxP5%Ig zYWG=ky!j{GujS(mb8>GcO!82I$7?hxFjpX6jjQNp8q1PpMe&|Wx?d6Zg*Bw}AoG~q z$m2->Z`Vhus+Z1n26Hx6WAK!jGtA}vu{DbE#>|vvbRxzj(#nfP@`>Ea#a{9$t6Dtk zD*haY(nYKh1z|nRfcn20@=&wIe-3#q%ofuOO4pX73ISWl0f+0ZRIkeIC9lts1p zZI~2XCWSm2a^*vPCz0!@A_!Fb>SnDeiq(B6lP)X?aF&RG=N&YRa}^_S&mWq^Y;4DP zo`2P7#Aw;7ZU)|t2?1+s68u5OdG#Q#~N^Lbi=aa zQmCjyBERzFB*9}b$$z}&;(+C#vJlDT9ByvPMr2J{5pl*HsVTj)RBsHU;(?w~IJ*s6oS%>Bmma^AtS%Q&DFS zd4zRXHoQJUxv>{xhP4%n?Il=HCcWXrPb;`W$z7eok$;Xo)Vyle8 z`875>z9LE%j zPVB~EYBj;AUHLNdnH`Qd*;FpbK9aHwMwoPW$n%^J-DT*9{r zuD0x`kl@LiVh+M|W>aG#gr_D%p(yXd5HgJLq(PMs)TylM1nPMe>ekM4H)ykTuvS>s zPHO43T4I0|QudqbtK~IZB1nfQj-S+hHPcyD>n74jJBKmGHcN!IAWe;+%*aM;N--sh zY7$&8+CF2wcMWz^98W3jk+WBpY!$03$jOi)Kptx7dsV4aeG6SxrW+-h)O;z+wj+g> zu?MW=-7$U5jG${3N;wIO!tc4BP>R-Cf%GQg@@GR|Hkb&eK)8_xbOMF0EEq+|jb~D& z)vEiQg3Z*@RISKjQ~uqLbqIAS;WmA{JV+-V=2!m!a`a)u`L_mS!L2e&aWSHzVT(-G zD;GK7r`9;J&O8&p-4eG*l}jF`>JKGYR5sM=DW$BrE_g30hrxAX=&$g|-Kp{wc=;#i zKxn$E94g;ODanrnxQtU1CqfqOI-T`I)vBpf*&;r8+F;8oE+>+98T>w_q0%OYQar(k zi8igfqItuBEs<N7qmD&QF{m14=k2sfpigCBFqmkN` zRTEj~uJJ>A@x` zNcVCw3FHBFzI6d0q)cFz-wf`aSe!zfnK3B#z2*?UZyUwvjBe`Qa?q^AC>EM9#xxWa z6$*P>_Ypik>elaQGC_@z3sAdZAxX$h0?J@4qhh3BqYE0>T5meBk;4}#th?(nng&ok z!5_>Ef=HxgIkDif6d5k>o-@K)P zSUn0#h%3069F~pQD?*iBP?+Xtmo`PCZt64rypxS$CCzD)(xdH2@ag=?-14cIjSASA z(mFRUDz@%nhP-C7e0GG&i$P{0poQ~Q5$Y{C@M$9jp(L8SxU}Yz1~(}bQc{#bXKa#6 zGM)7W7|uD&7DbI5%6v?AnYH&`Pm~(?E^x`EQh*qo43pDQ)1^X)6FgO8k|jynk0}wP z(Y16&ZvGdDUfqunXlbh!Q7f4TWojPp1}`A)*w-59ro{BlI>%wkw_*J>M~fh*TL;Yd-*XOjwtLqtj1uSObW<$F+G}y+(@Rf=+A7+RWfO9gS!6WVas%Kn zF3QZzyS|M6S!2FTFDgXAq~B(ZRW$g7O4(S5kPA{zZ;I;TQbNgDlzw;2DL0wfN(|a1 z?uKD%Wgisr=xU`Ng2}`OkyO3i@sk@SFo9?mD8nqc?AhHu6MD)}iJU)A9%&!gNS_+k z=8bGffulT3f%M9nHTQab8`E{*}|IUS?FeMCVAd@$vygU$##hesTmS}K-bI}2`3P*M==l_SO;Pk%CN{6NGHS3&H?Ru? zYhKg`j>Y0Ko2yy8Qsm3&Nm~=Kf5)Z?s@*_VQG%mVlUy+SPu>6W5!|5=cVXej@}+;* zU7arkvunAFc(b|0vQpf1Pi98B9cq_>1>yg~X4W_1^i5)vQxU50l*<4q z9|*nq4@EqAXj=_LIBV0;9+cqq_<}@fymr1)y}H9%3Q)m3Oe+E;yHya@Jh`gLs@DW!t8o7B0C2%jH?V)MmRIG&unBx)v%UW#c0< zvu`&o@*i!hUGQxHwRtLP%CX9K1nJft?j~$1`XPEei2Vqu7QXBpUGSL`1qsWv@eP_(YD|IVeLmq8T z#0RZ}zvVcszQ;K=@G$GhLHx3_#*_Wb(30126^OQd1W?9mpx&K?t#5%>nz9#fg3^07t z@yP+eSv%R1c#R4z`N2W!bwVJ3P&wpMC!0gvv(9s{=+CZTMa-m?X#UtPzMjZw!!;1^ z!LA=#uKMZ>;M=zU4~6Rsu;D~w`>_7%IfE}LssgPd#GNV)A6L#@VAjvPPs%=v`N)vs z0ax&r-EK_N!v=NBs@#M}u|(1cL1vbCr1z_hiqWlqTS4l0wCek*r`1S=RR9YkY2u+Q zZUbQ^Wy`b(mL?&4XL0QI@Gml1@x>&uTIs;Qd97t?*-j zmewhbs-<^UoIKN%9L;-Tf!pXM`nbV7YEE>f0e-l*%V0uv1(sb0fZq@Wo9&%CzbgRa zJ1!(--4j!hLBYYQgeBGAAb*E7I$CIzT|xX8IAI`Ih&+pE!)BVYjUse~ zdh`UiAUa^^rA54wJhYZ>)VsyCG-AbR+hJo_Id*&d=Uce`YKrEa?F#=B4Z|M6)CclUyc{0Sph+s_LBek~k)VnO}eYK$5iRE|`_Zh=Dct zp>8H1h**=prrkKKV=EAz8>(qaJF{t~-CZYtrLekgLa%kch)JQEJhTPOvfD6F@uw~e)?}79Lf`^ zy$S1K+8e)_ckPOxqhGnIk52)Po|sd!BLVuix2TpKHa{BGTH@m0(^Bg0r2PBtluw4V zB#Dvv4`uzMK}RZWh!m(%iT(bH9HHSuyFF1yye-DpVhb9|fRdVL(M1zkqi4E5hQ1;V zXm4yt?XcnG+KgE$2@Tgevq58k8BPz}&yt>tj4&zKbh7-1LS~myqNSyqbh1qA*l^`Q zn1;a~|F0trNNa0QZp})^s9mDz4PDF6{!?l$3-maD=^q_%3v3cI`?uSTvTIi=VU7Mi z?B=wL9x9<08BAJ{qf$1^bcP5Ap46m`3|cym-bHt+=9=VjrsZa5qQSIsV$I1YPe!;k zP`*{BIYh^^WH-tU;iP4ISH{HQ@}X0d_>rVf-{pgi7fSz5a=N$hJ+36kg*{)#8?*bO zjfb+x8LwO_+@&ssTJ-SUJo_M)yiFrk7eY6DIKZI3UB)+p)>+Xffu&8eSho;Ls)&`(nt122FU8EtgO z0|dLe+OjrLW$-?^QBPqxvHTwrowAc%Tl^gBm-o6m&(NJR=sm1=v+ghlRcTcUu#()Z z?Vxs~6uDH+2<1`TgThf8nf1NT8Wb2{9U^@2{JfyjrS#(hP_bQU)9~-pl1L*Zm(h<~ zV;$jXhTGLvJ7|$pZsCqHj04+ef7Vn)yCbLX0FNfVx5@E|Z1mH2SzNrzh7L_Ox6ieS zAWDOG!do+64-9>(n4#O6Ap4WP<&ub#w-Ts>$*=o~dP-^7bl&K=WXOcruzDx@jYDeF znx5FEl2IFHGIlhqzGhi(Pw}5t=E4lp-!Qe@l5Z{nxB8(8Q*BA^*sN92@=O#NJLmbq z9m7kxwBV@;tx1J7!n?UR&@I+m5CH~AjK4=VRs z_7rm-VE_upf$=169_PnaLSxJ16>UmDj^0krDr| zG&D9aqz?7U8eH+K&ha+W;ACH_naupha2n3{VrXkdhxRv&zu-Ulhl(NAL=2pE zBP9lCir!X7*u7eNjud%iNf?JG?CB3aFYsOEiWJ{f*PzU}GRcQIvd>Nlz7pZzU`M~! z>@#zV(|0uws$n%Xo*A6>3ORp@&3ilKg0Y2#V~}KA!lPPCj2JS-zm1bMcoPoytu6m; zN3RHUXZ`sz&Q40?fcy`o?}#J*CTd&RAa0$Y3LL|eQ98eAF~%^<4Nf+6pvr!XMQ6;$ zBUuY-`e-v1MY}>i6sfMv7W>s(>H%l6kmwDP-aKY7jlESw1Ut)5ZEzce>%K_8SLl2JBi zg4jM=>!I@>_Q?EEjX8ZFSE7y}rDf zS$>d{Q6gcujMVuD-=(loNJr7_Kjk~3E^{1Ns&fJOORCzl{;oB#SLO>QK<6S{H?~hN z|LrHoAMv?tI7FDMK0nV1mmufk7hJXREc(O>u0ypM4V!mrx3AvPJ!Hw$x2VDhs9YF-ZrhVl3sH0+daR z#R2NUNtk@qWWh>8EWW{Q=zzF!APfVm3o`tWq$#NF~QamYqPe=Mj^P8HZ=_VDqk6M z80}tC@c&|XphILlv3w{2T;Cj^NkkX%f*MMiw@%CJ6O0?kGJ7AW*DFk zz@t7kHAe0YYUB{xgnxn|7obXZy4;7Q*(iDwv6>b{Y{4Ntfs0SfW-Y1 z7YKZ{w8o2ybW!7qxNt6EiY${to6tA$U~Ic5cUg$2@-15{&Z;Ua!hpHKR5@+gp$MOe zhD0;?Jqqv`yR!+6#xVaX3ula3mfSmq9K)dcqV!L1W$B)mAWyH_kZtL3JgkL1xk#Xm z=GXbdw;PZ^B$nY`dK$2nyy5}%^Bge;&y{%{mHvxX@I_On%w4^+Wj#JUV2Yw9DhJ-J zAA0?W36bG&>vZ@0*QYrukoJ-&hP@+~gYCAs{D*=9q3YR>_%mO&nTm~cn-GoU(zNB! zpS{s#viR;0NbUmE`~9ar(?o_icu2}M#%nrpnstWOp&)iCPboe(0ljjY`|l1HB$8BD z*i`%5R3N6iSnB&(K7!HC0@n4fryrfso3nks0H!O5YE-T}ne^>uBrG;i>JoDA3OJ)w z2#kmB4{w{(=~A6+0$`P*ZyfS)jr_jXq>~jsfs)jGPNcM5Gp{`~K!ke%$$LTurLl47 zxB^M1T1Z~2fn_;~&$g>R8mfN^A|TGcv$=C{_uqYltFMgY_Bg1vlM`RQ1Yeazh}J2ZXo$S zC70KUNQHc$nDzowj@Z-JXu#xe)uhsNBJF-@o-9we8}iq62~ECAdX zEbdEf#24{3a1NR&R+Vgp$cP{E6)BwhCiicW(6Pk7nvcDW@$lbM0j6G5Ue=9Zr-4NO zfVnygd;JbFL_x<$i{74yBH8MzgN3USqf<%%`?!F843ijsM3(yKYMn4+r&!&jk?YYF z5YlXIL=8#psa4E%)8G+>Mk+CeBUU;$3YB*w6M>u=*%Q^1#8~j?g@1;zgen43xy}+Z z=~}1XG=WSCA<$IqV(;P#_SZ#uSs5~iH7lX2ui(}S@-rq7A$R@+D^~gSZ6nUYakkJdkmgYSj`7pUn)?A_x7}6$w>w}6(fhkONBz3#Uuy{$fqNBjoWOyJ>Iats z-f*GzVsi(n8=x27pPA(}=5G?EY~VJvX-RF2?@7p+I*_r8?8ebiNBoY7&)=hVeC=}H zQ~@%~pE6`_)TuwiSyHb0IpesY5KGHCTg_TS#KWF+SSAevHd)ggaP&q86Qwa;0WET@ zMrL8{CmUJeHYt}>T9o62KXkKO%1Ik-gxyzY+QaoDl)DVQ4H~M%(#w0++x-EPndS~f zTY9an^g5_mG=tn5tYhJ?6R8ie-yNAuofv05iVqB6i<&2Cts2zbY3Ygv2|${ShgEr zbATc<2HShc)@PDvY~sy@aLqY<~KuQrY+C zEgdhbannuiDz-gUiX%P|aWZ(JSW(`hyOGGKO26SqGNqC@C}h)k7}ELXg7|EozG zDrORuft=Yzxrm45P~L(QrAMfTUSgFgEPc-)cfqa-`#m~^2W2F^T~39nCfi=9I!_BK zu5vSGQ>vqVY|NftjKvcDslRC9o#YmvV0Y<9VYB(v4M(o~Y8~4rv7k9_2R$o2qP$XT z0iT2P+S^LXo zm+WG4{8?4T+82*w;%8E6KPSRJLw1@g%7zmYC@si!(ACzF$J_cRS|Y5Ok7;~{#M)N%GTk0M{6V1~#;RB-!?lg| zclC?yy2fg-mYHOA5J5q9@2in>tmpCmK-~k2kEdL7u?Lksi+i1ubhS) zOruQr4aCC_2YoGsp}!U?)5&G4oloXx{3bmV%zaF?JshLUQm!AZjU+7Z^RCduWFioY z;obGRs;yJYQbb(se8qGau8gzWwZC+TEdxY&Kl;yWV-*7}Gar{F#Mx*gCP|-+*2b#a@oaPjIDXYOWj_9^SIs7f-h$DSkadwZU# zpKSm2&i(cM4`uq!tf<*rk@GBk2ep)x<;m*TyM7{X_#XM4O^CZSBGJF#Jdn|sU z88sT}ATF7fSaHlH4dOF!jhwo-4%ITzSZjn%$4R`o?xe3zdP{ZX zEjPcSGKxL3&-H8p#8?I{2@*F^1&u7^0q-#@jn#b#TN)#X~6vdf%W#k zpy2-h=edW+)92)uu1M(4*j4bo(-Y5gu0)FC1I2ODO1;@l`D%S<&tbXP%7$Hc%BgD? zb??C{!9mE@4a;#=u;ps5l*3c^KS{4A(9G>~$zjaEja~C?v0zg|&myz9@B$=lO`Y`N z{6CbxPfzAI!s|{EFVc^$r;gD5ke>7>%KuPiHSdWZULAzw97%-=uUFqUJmjY4`3Ic^ zNC~abI6i^$Vp1jEp41lwl=YMhYp_NH+=wkOIbA0Tku@vVOLdN=7Xmif1==s3H>6(n zz+cKNuG`N?z@n+4y~EEm#^He*j~*crFZi>`(63KF?xmiDr&g}Q13dpeVIQA0NYsvO zox{jkpW_aDR{lw{fL0z6;a`(jK96jBSg_qX6_`5yhl2PIMgK`?)uyj|d*mEJx!Usr zHi}d4yPLZ@Jh5687+J_+L&@Xij@jTPL2E);8NzSMIp~sF2 z8;2~l8;5!M;33!jz((cWlE2KO}X6tkf(wb6<|!p^&!^X;*FkUfw{`HUd+^ zX@+-t?H}r&Gj9r%ZzO*QI;qhAhtf&29_-ciABxyUh{V+MhfuRnOR1svzwmUPUK$0@ zGlA7fKW_FxvtySMtWVo7zbo*)U#wOG%))MJM=oSF|_Xm~K2J!c}gW50d!Lym<5wg*)Mdk zxQj|tDMD9AqsGhtED6ILjz`t|q;EH&6%QN!_jRsrQt%t3nyucNm`JJ2Td>xfXjN-} z6XE-@r9i#=(AFQv-L1mIaZAzHw+Oc1N^j4q8e5&Jd5b+bWe27nFn z3(5H;zpW=F*=l`$3D8;>4->S>D`WwcHhmUfXI)K|I~spC(hWcfKe*V$7XbizBM;qRNe>8B~nDCNv{ z4Q{GV1Q#rhrCnCej~lG9*hZ|Nc`RQwpkr^aP~={Pet8ZvS6tbyp@v`o!*WVtZd{Ql zRUD9YnzTk6__qPnBudzJbfuLMj-js=le36YO#1}$3> zLP;B+)7p{plY3Ldwz9v+tWUr@kPV44X5Tyd5 zTw0vMFo$YJp^{0IV~pY=v2~9P)$-Y?%u$#6+a?rp%X0=2gu@96;{I-nZ?px&f3SgR z7zlV62Ozo)R2}C_(ccKuR4#}aMCZ|#mVB#GqA8n{&9q`Z?zBp=sTvqrlw?$s(5Fs~ zff=U)F096Q8JAPA9I1}hmuvFZx|+UF%ajm4j8(pCAu>fsS+mtNj$1c;IN~mD@+TM` zv=M2!K-H4ynr8NKykPUua#_P7u8=ffC@9Rb=fOa`8MgLYm2rRbA$AigtZRwKz8 zb_{azNPTasnhWAa5}~EVJ@FXzvmg_?rZ|IDM;+8yvK;WZdK_D+71|y4hIYfe?CGF5 zeq$r&$DQFs`Z9pSA;Qt$w~sBBzgEX4iUT~tk>xHySE|WtI)_;uo&nhSftP(;gj#od zA*JF_VzRa@$Hh%h$snt|rf9T%89&h?X(|_L5-24Q@diB!Pn&K;tneJ;xWsd%tehjM z%*MvK`9)lJzB@*GUSyd>I#eh>wQrEQ`f?plDd6q#t6X5yofiJ5to}08Fru556FU^& zC^fN*a#VeiZC+wmCVw)8z!^(U|4uq8Zqhk<@IMH#mqFa&q6mwgOyP37-A2g;Td-P0 zUzsSydj$^(Qmu;0428q)*WBMbiF^g_vM^$+8vTnGz6>^AnUOoTjUg4!|5@lq#u3Eh zt6y=xbI#DI%9$Y)_@!;g!0I8HiSDdwg_PfV!8(T-TRMDL%57#);D9#~;CEEXyyMP7 zNU`{L!jqfZS7#D=$uc#Cxausj9kiw7i5`b^{szWpXkLmRz7(f`@!t(2>tqg(Wm4BY z-p;0V1v`5pwk*1m1oNY@^z+rz4{(2)vxsC~y$6(28FVg^WRmr3S#L0_8n-IadY;0|HFo?abtyX~KJ=a*XY+@d?P3 zNFDho2GjW(7Q9#j3wuM%)RWm^u9?b*x(S`;oO)D7Q+a>S*VsLw)e}_AZ|J5mMm!lc z6A8#47_#yDX*RI4OG55XoBC+43E7NX3URObD>JeOm41<9L%X6-gOObCe|r)(|g zj{vbD{l~2Xs_g@K8O6Bir~-m2fs$FvkckcsgEV`J4UZ`B?htC&(3Cz{r>Ijd%Zp;o|H(8lDW`jzt=QKh+&ML!-h zXBi@|^w({Wt*MuW1Qm;0pv%pnDc2BkJX3Eey?G!Hwv&be>KfAuc8U$JDR*Ej8Fm@T z@7n+IAv;6I_fnVg%PQ9->2&m07U~=P1hr+warxN1YN%gJ^duXYiERkVXdpjv$HB6M zVY1~R$-(#+G{`<=7Xf@6=TUFH{I6iy4VIAq!fH|osQNqB80D*CI)6B>;&e3LKzDMTS7-Bf5o^x$aHkab$YuB=}T zC91F&4;9rsgSl*EzSPE(yhd(&IsM=pdH@28mASjp zn)l8p-CH6|9v-1i{G}o(VZkb7pEq6d~8S5 ztjvDXj*0pgg1-xth9!94aJjK6L`Np7Opw>H&4kU#Z5>28Bxot^HJ(vm#i|#^*Ahv7 zZ!**cI6^I0NY8(i?p+XhKOyyGO0}x=#@v?h2yZYc-lnaRnW3KusdgC6%Vl#}3=0lP z%7t?y)8%^dbUBA1Emu0UXsOD;PIrVrlcBR(4Ca}fTwqmVhq(>xC|a z-<^x0 zfT)n$iT0a)(YQ0s4wlqLW>st3m7VmW)P{%X^D(nXVp<4>r3yOyL#N<}7uU2W4`(@=id*l2M$XQd)mS@6^}*PCFKLnJ&-$~y1y`N(+8 zkOQQJ3{QpJ*jDly$3=RmPmxp$Iv57j=ryp`Fd;vcT*F!Aa=MAk2^)3Mm9zve zW_`R&r7MTwHXQHS2FlY}ACCr>ddE`VCK%T#3du4cQ~Bn(6$WbD{XAMNsz{^a`%6fV zahPcfl=6aH0NvJ#I|7vyKmKqI>!Fi7Mw2v4b0SEm#@TJ^5x7+-RYi=+=j_LD^)3A_ z3<*)!mRBMc04nTvC8X>?xxP;i*RcyJ#^Lu`uFFljOLJ#4RP%rS6d7gOlnOK@s+MaY z$|VwYpb3EEiRLM``Oq(z`uD?xEDT8D{XnsVe2%SS&`@?RUKNT3_sB zQQSV3C)wC9Q(wU!%-w*$Rp-j4)K;K_vrZuOl41T$m@RQ2LyG%nd&WKZ+Vw7pebqdS!a>Spoguy}jH2aRBr&hu z=h;nf*V-)7p-7wHjMw`9;!5nMtwP%k;+a@E};6en8h$`g`+iT_K-k~M>x_rPHFzBg9;M5 zP&I3KRoniFfl$xqoOXpmr2Gq-i8Xm`I|yYDdy4EpuvPbz9lEL-BvJ`J3W+mzyA za-Q7pLq6dAi6Zl<_2jD+#&1vxAu_ zdfiXCAlRI6UAV=vg~MX`4vmdH(C>pIFwkYXIGyp)SH17zR+lCP z1+JV#8>XJz{DA})=k;>yMzCjIp-!Am<8aC1ULzY5!n7-<^gk3jLrzz1YYT5Vi*%Vc zl2jw3ZHeO1UVrc?X!l|TM)8vL+N*(tXg~f|gZFG#)bL!dV!x|e&s2r}R@?jip0Gsa zEAJTN*scTdON!fpP8GwOd32GmNOLBAK5@LNvl?%W9U%ECj&A=8BC-9vx%q(BVFUex zS89U~ldlv#%)myXtL6)FU1^Dw(16&TO=?TY1UE$_UuDEL|2Eg*YicSutT@fI&xW94 zh7Ac>b3ZS%ARCUo1wM&CWU{@j_fPO$&MuHDO|VU3+TgDsRw_%+Jd)cPBbf6q5o%eD zliRM}sTMdKaNET;_EF#clUx}3Pz6N%eCcP@F2$edV58V{({d5SeeyKbz`U3|%(CD+ z!aT6TCEJ5Q<(Q(Y2b#9~*h`ABTI4g6%Oj4+CP?ps*%vdDsH%;#ac6$heP?T`lrN0h zNJfi-608ekF>6{e50(Ac)*b*z$Y=ndSj}IPK6!d zT}~%#>HjTFaG(2u@DCU1a?EG!^z&gi0{GyE;-9)J{-_k^SN@vG)hf8KLU0%WfHhLqAgMnxtQx5S|cNr^J!Ge#7*UpM(=x)6q^+)*!u_nK54J zjX2^l{Z)%;UrSbc{ek+YbN|zu?Vw7m3Tjhyk`i zenxecHwQrruI1tE!#I&A7ec~Cf4UX76R}S)Rd`cQ+fqM%j|=aq)+c8%>AwI0=gJJ= zD$*ONMJ_@yuWbIjF+vY(N>}5|lkx(p8F-*b5PTD-Xlvqv4w%yo-{ji##x7vo00we9E?oZ8d2JdQNdJJXW)7u@0;|-EUMbD`$Aket(Ozax!*m^PN6n+&-%{n% zif!`m!7>Y4L22x1m`uz}g94z*z&PI*eirxiuM%oVH$Z2nMp_g4X2ijHz@>nItwR$F zplVJRY}!rx=`uTYXq5A;{lk@h5x?qa2KjFXp=WPYOTY6N0|0`24!pU&{!D2jHZx@Q{YhQO`GJc5PHsDR^_UIJ_)8SyNW=mZ{#b)K5rd)@wrk+_-Z zq8z$GhY2TG=o3hh14A=sgj__Gl(0;{g4?PeCGosYjo`!y79yb*?_c$848@@m^&1B+ z|8|@ii*GA@Yv%h#KYfZPg%@{oA>{&GEw}8Lb{hKDixOM|A#=MBjv=Wk@!267%UVsvvT^!LhxRH2Mg-#D@chcq zHE7wTR_5RcrJ@~V7Cc+Ft#qqVVH^!R%FKEO|A(^oxR&s=`ck!5hZ9#gB zlDBi2MZq5ZTS|h}p~74xKNgGU3)&))!04Qr{gGIxg~iarw2EbkB_zUu31@E__XMbk zrU-e&7Od*@+r>&nJ`SBnFeeklt&B9aP&z#zzo5uIkNZl0_I{-%wu7HmI!vKzp|@eL zc8k`27Q8Z^MMd1%v=aXy}@v_%KU=#kgYD>hz1-Z!N7S%ut?TYYfo($ zEv8L$t9{@bL9eaE432aw79)fPZ5}Ly=m;5bYh$XlS)BF@N<@-aU!ayN^KgrswdtCK z!k#}cL58C*+;$pW$xKZ$5}AC9GjlUX)-Z+~ zOE-s(F#&xQAXPCHt5|;HE7$A@YBig{V+cAn=)E;@&EZ%rLRxu71?;p?%?hBi7@7O| zo9Ng5Rbz1+k?;&~yE4&MQMbao=GB|&kp(7P8QSue!A^F-$EEZhoKu;I<&6A3E ztkIt=T!rEr(kEF5nF&FZbhCl-4m26f<#1Ecr1Mm|lx$b^_f|QT8nS#MCLeu4x~KNP zD^}z`{)fWgn3y{_-N3uYK`_eImckVj%W~&HZ7k=Wr!}M$EvTKyM6HP{-1E1%@o-1! ztZ_f%suxP$ASXllQNJXZ=Yx;Z4;l}iR?UlG zj_dDUlzD}=jmA2duhF~$8AXM~x5Ses1_$}g;}Or6ZOxM?UAM$iR^)3BNoOh0bJ?9* zew^Kjna&$zI;4XDG^2Dkuy#;C5XRY!s-)au3h2@%f9jeB{K3Xke@C@VI&62|%fofn zP5)kiMEr2n{+J*)VtW{5lTrKJRX|nVbB+P)JY!cyZ@z)75Y;DkteHwD{9zAeuB#j_dPuCaJbMXB#wPnkm zpxep5>=8{v2J1=y+hSI+R*bmpDJUunYNx?rMIg``lZ?ADG*-=8?|Cw@O9_`8u6|;$ zNHAXvZ5)2|eh81LH7t`)CJW!C%1FsAsv2*+5se>zuV_QI`W9z7po_no_6Fwiy3a&4lV~GQrlhrY@t84}A z`%#(%Fy^xeMy+WuNU`2{8nZ4vFMEz!wAdl|m&K33k+nX~Z#8i})I^4XWot*?qP%n- zKT}guIx+I1O%T>wHUMeX!(+z|Tg*9Yib5?g{?!kzEoQRI1VWW5;)&?t_K~6=3bu)5 zh=`2Nf#_KL*)1ONB|}pc%9N%)2T9i(Old(7MF9xJw`_i)=NXn>8Y`w%c)=RUJlSK2 z3A1vb^ho2@(;EJ5p2EzjV4s86u0p1O(U5>X96#8GwqmCx#u!4!et)nUQF44MC+K2 zGhNQ6^iLBP`pYA1MeH*C6ad7lnn>7;_Z@M!6f4@a1Qi36y9GpS>|!Y$sDNSSMa(Qu z*Xf5MPhA$w-U2i7JV>fLdvHRFb$F)R?6!itJ=h*3O1Ix{PF3+coN_ zXMw5`bAL5Z^8ptC8bt#T0QC3#hU%I>F$q1DU$HNMe-T|`R|HLC^t(t_h-YPRzNsdK zv0*Ar(7YckHp9x-A}+8RHy*HA*lQ^eZlPnSHsz{Z9|(>=poy>}E=GQL#Nj?s^#277 zbV{7hGMea%fu9y-!xJf~-v40;*&M$<+t=sMQ{)qj-qnB%NOuHR5CYk?g!ie*q>Vgr z{1!v`P3HPi_&!+7iI->|fw>H^@hg>y^|LcF!&eb19ilpz1 z2C}UGn&gL;(4P%znUBJt+z#)s8468F4RpLMq~VoGRM+s^qGEO-I@d8z0v371YbZ<8 zn8lunOx1X`7$PGOk`MlwAQM{IlOtL+;u-e}4%q^sXv_5_(XkMw=~(;Q!6Rw(BMLQR zV8Vog3G%HaZaoC)=1NIx)|Hpjd1+c%E>hb#e~4Pbqlk7CYjm(?35tZdF|h7i<4=R) zIY~1lYg=uTBSkkkETnbnx}1&-s}9-I@ktk)S|vgH|DjNEv1sZ1I97*83suZ~wv4Q> z@!gJHsPv+bTlo1d-8H?cG19pAJEyekev>5UWg5-QkVK!_|65mZ1}asdUOpB zJ>hl}5#7f%tIU9KyiQ8+X&?9QiJT2E!%ifGa7E~|PVZ=Tk z+nVbhFn{LrraZH~-L2{8e^_Th!DoUEd1?ubB@78)(s(;PezVLhl}=zHn0`gRb8-|4 zWX#eaz+Woq)cE?YB*X&W`?%CSzLp_#OkkM4YmfFOZC;YE5LV0%7b{d?(i1OkL=&oz zqEY+Sd+Wa8pzD7jJEIR(0#mD?5ho7;y|(6mY&z zckmew4+f(Yt0$kkeYQ6d``)GOLx!0v6ONA&O(>> z)v2R+3r;+8IEgVg)>cEc6QcgEZ`;IBh^k#J@}I)yYOPkJt`!jV^a&67DMVOJv_~Nx zfd1MBp*{GXoN%bShkTcX{~<=cE>EYscn|tpVrJAIrrZp!g258h71Xudr?_WZm~9q} z*nIB{e-7hvHO@um3IBnZA_Fx8mNrd?SWwph@W)I{2qDK}6#1LUNz$F&c=H{}Jq}*v zJl?|@k&-gs_r-vV^^yR0Z$qYeUH%exme#7QZM2ZX=q#fz+z?WcZWDHT=~%i#neIT~ zsD@og%rVgjV1QqP)LNd{k$nU&S*Ua7BT}(rf+DP!Zq(wa%8VX|nrbxLR^Fpmx z(JFC&TW~GVCdKb%zw?#`TvK)-{%LYkieat=eK}!Bn*9WH5&2D?Ujwt;e(L5eHtzc* zToCDnaLe1>sq;;7VF4mPuZX}?Fs1q`fT41BI8`;s8AdnFR3`LrEE}p z>8`4#@sNMBeRso}0x{9A>8#Feu@C{*8s5)S3_O%s^DcTYg7Ofw%+>EH;K?xVLS!Fu zZiif)tD1Xx6WBYH%IHAvE50iiFgYd3RbfjUgZ)J_U{y^v+3M5^*!NkGHiO7=XNlRr zBEIL?Tst+uAU&TC&?P@$kSYW`YRr(Ez}qKco@~nG@5;KG#mO5@b5=7_-rufIX}B~< zv;RD5gQ_IFe0T|P)YMi2$cm(u0Dp>BIeh!0&vqHYl%Z<7XP&MX@1|A6QM3|Pw{3q_ zSoOD6=RtrE4n}r*WD#WeoU|U=OvrbRMv~7`^1scXl`g3{m9$?NVV>qf0DjUl(Aj(- z@K_G|+!(*U=~|M>!@3N#hXz08c}3SK0*VoprU@jLe5f7cORbH!g5*$au72(h%>sN8z7n?{ zt8E`x!!xiypHEONoxdoj8`bt?SEiO`X>*8AEsiARiQ%l`OZ2QO8@-@SD~xN_A5tBq z6!`vcd}!OgqzLTKr)FXmFFjO@2|p*o%s$ExEWKHX?EFEEIQ$no<4|nkI0A%~yk)hF z1hBk`K$2U%TLC?f)f<(sSPXe;SI+zGMqh;gvsLh$XNyDNmUwM%i$Dewse`fz7nHKs zy!8SVk8RuG&Ew;wOPXs=&?9HX%q?m&Uz_NbC%)Pi(y&lYDgm99y&>8;r@J#c8^d zk(_R+trDBGI&YyWl|lAN62o?(^QBPoDZ}!u3_S?vC8v&=xF$vDzyRA+damiDfZp41 z*N8`0N-$vOIoKOpCi#iq9o^^!#bRk>r3+~?}i=QE@I`G5KVgqi0RU)K87pqwNK3UCXVkQ?u4 z)UdU=$LjMr%BecfurkyshW^ncnBOVY60Ht;Nq+1%j+8e`%@G#qVL9H#r|2-h{v=Xa zk82o4o@z7wev5I8g(DZvyX=&)g+-^5wkH-(kAfl;DF9B9bVP#-QQ~j)U&@p zvx2GVcnZAY+QKF&9RiuB8VJ5^M~9drrC8x}ULATa3_colm3)|I|3lhYc188KQ5aB^ zR=PoIVCe1;7-EKw0fx?@JEWz%yOHkh5TqGWT1thXQ;?MSKYWMh-C66rJL^8{oZr6p zzII!w{*)*==Ff6qx!4+rO->A`0|drn)W-uPWHlh9l&?gpz+Yg1a;qf+*)Jf?$q=-H zG}h^!ZnQ(Pu~@qEl*s-a0_gy4i7HxE_IZg_Ug^@8F^c#jb#?c^o1CEC1{(Nj13 z=(F+QH*XKPU@%mQG7$5bN3h4qbvE_bM7L$<+AYP>y`S=Z-+AH$?PttC94E%%6RYw3 z5SXvWL7E(<7T-Qv7Z-(Cc!q8dEf0gE8ptcocBVkZ$_P&g^H-#k{2SHsCW~;^MI_}S z7Y~nhXIFO9JA19+0>#K>8liOl@%*A?-LDyHu zCRM}`PwD+t5i}kbE^feIGRvx+r*g%WVN#R79h$iTmy1hYtq#WCCUPMr%5m9!<$;s0 z;cDo12`O|M9cSn(nud$$^gaw9Cv*a%uTgWuN?9nTP+bWIrak1#7`LsPL$KrbJ~n48 zt$teJKag7+{gSiJp~JMl|5m477i&q4x=ZPj!vdgMI+1<~fj03xzY%O3)b4xW6?)sc z;v!ndK}|+*e$TRyY%Y_g3vnY;p-4es;d{o^kgx_Nc114Slq#QN9w8Swg}0nDihH=z z^}YD?M_RwW%By*!Tm;GUT5%dJ790N4DtC){tv%e|0s``iH@z2a(Wf^hmrk7 z#Z!lCa}<#%fEV`U8wB`m**Wg4wd5Z`^AZW&=}T!WuGjCbKjj z)oqwtL%OAMrfgN7`sSZsHO)_6WOy=B5urU&Hu(%(%TjvOL#=>@D}xbn-|KT za!U9Qg|ZCbH{y>b<%+_!EW`JsIfqs4?O#+Lky>V$<-V4cV{963A&$+#=;e+`6O~aD zQX)z5k`J5TOXYPCRYbvmD8g(DO`Z{Im`zg2NZt?~`L^ZZ$_k*8yjZ^WPvyTP1m4ZO z^9SV2CBo28L}Lc@Zor!SguI|G9hZL1v(=U=gV8uiNl&bQrAm-*6G5gzv55nc6`j$^ zm6~p8P|KUYQAYhN2&r3utiC1Fu>kR4NP|QSV$C)7bruYlMT4<;H^vQPfGuLK8RKKt z=p1-k&-Jmc{_n4c7pph%_0FdNsC-$9IvkHF4ms(wQ#zZPx~7Tjtk%uFFCmCg->GxJ zEG7_nCkn8=JV^|hO1WhHS1G5-`jq&TjQKB{fp%e1q>SC7>%H~qwRPUrF=a?uKnlx% z_6OH>{4oQ&5m#40&xX;-Fri}aRM!39LNMPKIo97IY>3=^ndTVGVP!Yweox>ivJcYS zpr0E4`IRzq2B%?nGV}E}cg7wkKN&!#MjD4Pegf~iiuz>ix)g0?`r;XSqBGstMR}1) z9z3|l{V9M6js9wUn)2eLk{D)FR14Oam;ueFTWMUHsFP|Ql)YiN&s!j8CfBm!m7x)> zB9?Ch@79{Xgif}#>jvYpUsn!7e((9%wf#~VCXXM_`Ek^;&k)$UIB9<7eXgsc9+&x& znVHh-y;)US^O%dR;#W&8UaZYNHrpBiwtbI$o9u`2{bU>D@0yd7#g8Jffz&gdm8CA@ zlQ9>3Nfp@s9+ruHCej#klCUgIq>zslvO{m-gq5EPeKpSTKh5NApsM_4#ZLTUc^;dy zbUkCPh<#4tmENlYkTGfC2`m*%)0a2m1IL#ll#7iO4?v%ynDDs$vf`)x{BiLX0O-rL z>;TXl(1DNL`&g-&O@+kEvg;yiu}+Zj{lbKOwS$gN1(xV&&4hYv1EDtDOnFI?xd$zU zW;tlMBo?4|FiZ9;liFnw?>zp?<1%5})t7P}H2d&aRv2T@FJdBM2s24>*nwM`icEX? zy0@@%=|dz3V~e&0@J|f>kK+%-$ZPll9Vs9ww~5cwnOTjh*SpPAU<86jFtU5J8C@4oJIg5X$8n5mP1DOrDsXAI3rzMF_`B5q z?(#~xmm#bwy zjv_`UJ@dF#*%ggfNc5_Nd= z*kgP%N0O?zfrOn!8=IWaUuO%u_@b;hs0GpN-$3Ri5Q3hI!;~`ta+l;5b{);?n`ixHo==A`F4@gF z0tH>HJAA}1iq%vVWx`N}M2*hsuS-0`jr20uqjfY}Pfnq#!xMdVmh2Dyy}TEEQt7yk zot~idKj}P(?5|oPX61+nTexqbZZBw9Vd8~Hj}!Bvvr59W^*L2S$Xn=lv|`vme5tKt z!1SG719~*;mN{9|ju88fnB1WPS4)V-psLj?j&>oV&Vb`h>c!&DIhI=RJG&S}B31TW z`pY}{{g>w(K}Wo&Lu~)gKOOO^$*>qF8!34db=s^`7#HfQwU<~PL&m;+*a12=>y_@p zm{i4|gd51i;)2{dD62N6BLnQdQ=gC4k?p^pC-^Ro8eSEA`MGp;w4+FT{l$MIAeqA& z%k^#PgG}4p%gRz8*D6Y`1>##TJTCa1_tP7=T0Y$0eUv$DPtyb^(*z4V_f;XT}P^Fo1l#oL+%&zYxv@~7Pvjk z69NG~A(xq>kWIqB#ePMyw?(g7_=)O(gn2{+T`PGA-dN&IY{aoavtb#N03}t%329w2 zM5>0JKPl`qF|!!!pbGJKc!|@W%yqNfA-=WE;HF|4vNU5)cFA-)7q7N>uQeH7-2Uq( zD=jp<{{8%t`nbLMe$@~_zY(S}02%vuZ)8L55A~9hGhwpaUyV!o+FPq${>h<&xZ2Mx zl{hI3(o>z$h0jfhGG)Q{O)Oz;#N#Svh1ZLIq+W}5G3FFq6umUP<7|ngh3iwfKI^nX zPG<$3e(VItocrNAmH!jq&Ch?MR@3Loo*?gNI6m6xXbdDLAWWefaJ)aGgI+5htZ@ z17ecKOsi8Gs`dM_J1COD`~{BE%+MC;8vbdNmSJO2`YnX1mlf>jd5 zjWMfS5@{-)%1KD8*|CCi~_f%Ywq?cBx)!{W=d>bo5k`(g!ST+?Qd~| zmGDJqcsp7E@TFoXpUG`Khc|N}v3V?!*;+JC`jdy|i!($*qK$;o!s^lYfJ4PpCSuUMHnZIUJ1ki*)!AMW2+{cl3Om{6-9(QlYtYwFQ3Mv(xfOf(aqr zu+gDJ3dCx^tJ33{vLSIzsKx1Ue|ynZmiwNxQ{Ctq@+-YCe1Fdbz*t#OelbH3Y!cuZmzH8&N9t5F+8!bRh&qg zHk{S3C7rEgp)EHWtTiTHo04>MXJy5#Wic7deBHx?Oh!Xs<+|~E+^u((7XbvpJ`M+b%gb(#XI~Y<;X&_U)!3zl(>s$Y! zwAa+6TQ-iCi~1g(_X>X>CP-@M(CZy}O&wk4L!oI=mSmxk?2yiXpByJT;1b^jslT%j zT>Av13fp18oX)G^1AAKQq*J5h&nKv72X0Yvgcf@{sYB6a@vr_F;v8-4r20jjEfL#w z0BRE2SbxTR*cn?4afO+9;drT*Go%rndD&L@v4lgpFR76b4xCF7R3_D0_JTWg#z$*B zgs!v{zhfRsQ5KW(gTFN~BveJ#1urLyw{)zr8GrTym*bbX*<+HG_ostA%x!Q z)304ie6i|{qqdwroHaRoTle|6Qhoo9sUM)ZY;)&@BI=kj80_PcWQ)Z}DqZ%ezq{r^ zT72MQ-*@L>%Hx5b2=;fCR+a@B5(KLT@ooT;FH#+jgRN*P+FhKoY*VhAr~1XW4vT?* z842Hv2fGckw=dq)srUJj2k7v{%Zt2bNR5uYihjgc6NtXRvdnXqrbG#s#ju7l{X9X%h|E74D)@N|DJMt3vn4^A0z1Ug<*H z7AEqaiYAtFJ0wBC8ts{EUG|8ySk{_d8BE^zmSY-2m;K04oiex0^idEi!yrHeSC=KK zLEf7ERidlpXVPCxx;$H3_!R%4ppWC8y~{hRn@lQcE`pl5%xCM&lA6a2U6 zS9s<7UJM01YqFan$)K}(N$_`9uZhXQ6Cf|L6A4t6T>h&Q4=Hs!T(;TGYgQu$>J!l( z%g?WP7vpK=tfBU{aDWKf-i;scx^8?QOZfc;=iIAs$xssKsqNIaw@vdg!kGV|L;+gj?9u2!J1JGny$O!WuhxyqRR}57 zlQ_S~U}Pn#949QNd{VCG1=Y73azD+0G?9i8F`4J- z8eO|(d>xS0`AlpVEM`h$_n+?(U3-~9&#TUVOmjgf8gs89g8I;Bo`j2`KXjoXg_06#jaTGmzYxQk4^8oCqlxB zEnsnRVKx6M;Ni@CfpR0DbQ!yhAWdKWy8u{L$5Jy+OtYiON+P%m2e>1Mv)kWj8g(lI z7xms#>s3(x)jJR$z{M*r8eyGz8r_~^vElsmd#Ee>)M;Td`ksY_&PGup4pgab$Y@6H zCRBfx?Gf(cndn{gv36{MOUz75D8w)x&Kc;Q zmhf8zs#rmjJpyv6EdF?cPYdn$?zOJ^Ny@4x89Bf*s|Ztjen(QaG8=-p&MjU4G8Xy} zqVZI_^QHB=sd?Ab%p_G|(>P0=C3!iDg1!%4$7E3dS$QLf1}Zkkv>UjP-5(cM+iqDm zT(@||Hl~xS^uS`mtBG}7A1>AV>5o?2@u8q$*(V{1sq8nsg=N&>ot1}jGZ^S(h9(+O zji%IBt)&+8;l;~B_4}}Dx;M*p08lZez>(&f+UdR2xhyb2>v?pME&k_%k|K+%i<#Cw zwe4kZ$Yh0xORbY2n`UV>7PiqiIHMzKvc1G9->NAll>xJl7^tLP3QpZCTaF1UXk`e$ zP02d>(a5S03lQEU=IzI$x4nKRFZ!cHAXG4<{4$YpoMn)5g_O|fM0&8d&a)_Qa5@O@ zIBbU?Jy&z#n5gIiiub$mxx32iU$1xFq6n{^*~u&uOuvypV@v_xj!>4D{Is>*W2bsT zo=-aYL;Y+>vI!xfjk;jxU`ZpfHN${0@7@z+>=Xb!wyYxpsIj9BZn@A#}vKhFUm|yIXnQru~D}4lZt~7%BC<$}ps* zqax4g4TqCv-9G*4*Ww$28m_r%j*p@ z#G&*k>qHNE-rW$FV}?O|*Tk;Bsjk$OSntie^+IUhmKs@{)x5%0L*JNCv+pV-H^9<2 zXZukHb0p}p&eMmhNQriG=ZtOy0G2*9sfzd#&p|TglEpn;rWiUj5vlv7WmXCkJ=*ED zLYZI<8}aD)_pZ7d@)b040-R+=%0qnkq(8Ucvdx?3c=yAqLZ&~WwE_xy7*S&dmT11o{iQmvYDv># zPiDn}o2EtQz*eR|&6Wz?&vHM5_o7>9VkJaS<*&-p$7fz~o^DS_R?7>q&vr1=mu!On zAPG=YuJBkSmdmey@YX3{Oh0}8Ckb+Jy-A(*C1N%ABNL0-E0&MFFUwjEd4d>cBFK*z zFvQ-+tf*zEcLfOXGn!s&>IF&>gLrk(A52h!c0SC6|nJDyAg{1ABvr{+G(wK zT#LOi>Jn3~r%U$C ziH%-JV1tlAvje`1#iUiHoC+AYC5KW2GH@%tt-^VgZgG_vZcjQ1OeZ16KIS3oE(mDh6SZnDpb8%#4=)+%a&%rlKT}f8( zQi=XV z71~LbgR<4hRyxCM;FSawuktKm^dH3_GeVIV1K0Z6N_p3vV`M5hYoI)2Ds{2@F;QbJ zcNhFmGgC<3hTI*r#RhD<7?z*Czv*YLkt=5>$`Z&Q1m|_J(=yvfN72vNEE&xzPiSGa z*|t#s=yH;}y4Sk)F;QoirPXw8U>20u?AVQ};9S#B(Q9hr)(-O;cDWXdALa9{R!rse zXt(L-8zr@nvp8UhOQ?;`jKhocZVav<#(S3^nj9qbVM4jcb zs(_8jJdaG*Xc#e))s3Y(Z3UtPeMuXVe<zELCwJhfV!VS(>%{ zjHi5i8#vc8?jb}cQ8&8L(`hvrtE@Nlb1^147iPnxt(#A%EtNJC7qV^cjGm27ZKFi+ z<@~W?5~$FTlYx^bmrGu~z2BY7>lBKU6Es0;?fHD%!~F^Au=@>);OR{pYt<1L`24j7 z%YNxMhxN9PFRg2W>rSu#R*Oj{Dq)}2-Syh6^$V}&DK&-8 z3O6+ccZgNGm!L`_J1m_jfKgm;Cy)&o4PbM4s|7>g>Doo9$;Z8zXu<#lLE}lta*h6^TL)_Z-M?UXcaZwe$-NBPZ5+YLE*C8Ee+x63z^p z39k8>l&kp%C=#rYE3VOOW0B>C)j_xRf#b*T-5`0*uy9Z9-H|!W$E!14PD=M7)76l^ zypyP}@;hfu?RZp3CEob*D1d6`Q2iRom#pWFlh+8Cm0&GNYDp~qgI6_Iz~nNkGuYlS zc_jg=lt;8u1*N=~ERbmdC6-5PSr2G!GnTxs9+`f}0sLBS$*d7=IhLq}F1mg=ztVtZ z%wgcs#@+kt%G^8y7wY~+mLQ`!N;K%o(v4RItHa=XlW8?B*|k`gWpCg9`_Q)=7Ihy_ z1-H1s^oog-hG}J0;_sN7_D6)3d+iybR|TcGlb|~|gUMerf4&cpUHko4z)O~;WA}$@ zEfMJ$>i{4Zr9wbmyAfyo=4gueqa|5^cxtwg{xW0NWptJ}ZkGjeVb|v^1-{M8mMv&| z2%Da_hP$6y5k<-*vqzJ6H4d&BBoF zDxalQG#~(F>PnL(+f%$ok=HxmNS)(HJS8K1(zqkB@7kR8&@eYdbm2SJG>Az ziq^N*Fu^UpPT<=@u#mH*f5E9ba1?-RxE%nLq$x^ zs?*CP+{#fl^y7DjiN6bw>11kq#^=u@Y2`gjm8@OMpwaYCti~TaJ1x!o20P>ZNE?<4wDzom5I%^`(7A$ZLC%)XF9Np$(X2mvfON)Z|M_P`$pUC z=QY_wfT{sOI}`?vGb8GbanP}Z9Ke5dPTTY1lkvmv)2c0@G7&!$gT63aAFmdVBzwO&TD7ZzCLd%;lIR6ea>_E62pp<+ypd^@2b!-IzauC`WaZON zDnR#Unyv=t$SJn-Bb|;lA47IYLV|sh0qL1PJV`r+-TYj$r}~~1M^`n7L$N+xa7|Vl zl_*s!+<$thw)zi6+N5E2!TFx|f$wm7h^FfNopeWUXCCK&D5_Vpnh&8TPE?(NT?fWx zQz68WWqxC)vhEUm87)6MXi|^GGi!7nxR&})l}mhc7jdz#T*BhVXOE}c1Dd;=4zX{B|4ce1M8)r2edZLLSO0a8pQ;M+ zC5#0MvBlcxz7y0qsvK8rNIfnl#u!Wtr}i_4(t!zEbjvnufrS*@|TLBZ3SJ`p?LpfAleSy+p|2z0K{Jh5VP%C+Go9N=v zzb){>CD;DEb^YVB;-mwNyXrjV6YU?9r`!Kf-ZiB5ojs?#JapQ?uTM8EXOTZ?TO}86 zZ~OZue0!fhokn(jb))Loe=t}qncaE;&J!WscDb~WYD)oPk%&UtTuC(={{-P?MP0Z! zbI0xT-?i=O$n}TtNYbvDPATuZ7f{)H2et1G?}LeRAway#Wz;YWKEO5hABxoR@xhrC z&C4&WdHCiG3hiLxEnMnCT`KkY7v$#o7gzU$i>ZWi;tQh98DBg0sTCHB> zKa|PD`*$~EcZ*NV|5m!c^5q|H{91gsN%pV5MRM@LKIwZ~7tM(i+KZ7u^5OcOO8N9> zn+H3pXIj2AtZO$B{|42o74QBkCas0fpeF{})=^glCh_L}^JkvtLYu_91|zX+sYMdl zi%=Q#`JdFqL&KACC+&--_^X80^?_}zZ~w_P;6zPx|7qjJtikz-dG)c-E%x%OU*gSw zC@TSwz^C--gkwYmNN7n}6N5ZF;|4>NHpA25+?7HeC zVE8lBKjmK=_J41gtxeWk{iOYFktu&7E=VLYpVB|$C^X;HT%Y?H2Hsw?J8bn<>izk= zJX6lv;zJ3}eoeQJo9gD?va&N^&<1K>URFH`S;6ejqX;U-JY}zT>K#cTc zO;|BQvxkN`wlsf9viu!XR^=OX=?}4kyE-B-eg}35j(Qieux%bXvnX^bJr>~DrbRcVS>U2>n&;JYon z{jMz>hGZzsMiZHn+U_N10o9l!ZYIv{J={i(pq$<;ogynseGe02qM^R4Yle1o&EBR+ zMK70J_l6Mk<}qkMrizgE;=9NiBXZ+h{PB^zdV!VhjNO~YR3+y>6n-rVI{t3hTMT1C zLqndejIuu?yvC`E4I#iTLGxBm4g}Y@PWQ8PHJ(U1VVTyqQ4g01A~bvxE4$(rT3PRl zZw1j+$$@~|6P!KEWK5cQduSDFY09AcsG;6rsp_e>Zv5X`lYoJLPK>wz+&{BA75`mg z7LDQl{+)%~4*zZ50!+2&@nhi(UIjcAC^Ny#Gu_~NitwLO*cet&8wy}Fu`Hho-3u_Z zoJvs0qy@o~xzjzMB7(QJ_qlkiX07G8bIoIr?4>g^P(nK;Oa46_)FEDd$jgK}m^OSh z|8nfGa2m&%%8YS>=bLf!jw-koNmbsbA-gHM&EpKg7yescFOZ|0-Mt!GVsC$gnHSli ziB*iEr3@4xjE0w15$ZbeyTjKJloSW2M4ahrLlJyjYrDIMl~XKI=k~I{Y;KZuhz#>2&UE< zni<$By0KJZL)gYX6q)OYIHn_fB6u?Yv`X}eZjoYRtc;wCkG?5Myvm}lI9zv#bJzvd zd)UOmvz23H%O})f=`>VP3f;W{v9`MrA^Rc}t6K3h!vFZzThgiM$`yRFAMggVQGfg4 zaKYPHiexSsyOBDeC5y7xl+hV*tD^5xnFal{T!Ft&^3s(AN&G5V#oDvamXjM3ezG;S zmCPxP`g+%jT2?KEnDAMfKBMxqjZ42_f&e}oLUB000>_Yr>!m@SW!4M;r(Byb=s}CA zdZdm+J7O<{7la%Wam{n(lOycTUGM(eOSajb2bSvLw+I=Tf4OrF2pEc=O{3BJ8qgCx z6-I8OnJ3zlE*A6#EY5=LHMDv`spN`4<6odk0cN8?h##KG++49RG4hy%*~eBTlXnh9@3Dq#$7s zVGLkW?b4eg+jxboI8akgJjXFsX|ec&_lKOJHB0nF5gaOBZAE+0-qTa1Uqd719&d%P z`IoPs(CQSDk?n^0-Gv|$%lZOf1(Ott_~G_80f$q7AnkJ<=d?f*cwJ(5XJF!U>HY}ru+iuR2#iV`9wya{^Pxbh} zFY*%b>`f-2nN?-(c0RB41@YN(e;LNpNEW%c;&EHR8BBkSwu5ROGW4rZv>A}-9P3R@ zRT+kFwoW=d)|#RM)9u(AGvca*#E_;0uKK0q^;L<$r$=NY1nx)8l+*Wk80V55* zKrr*fZIn_aW-bOVuPPwP}UuH^KxM2ue>vuKqakA6ZY zJ_-{w)kZH;A6Yim176nD*P|Hgh6=SIQi&*#?iq!b{zrH_+mQ8;Vl&)QsBOun@+cD* z;8cR|leHkFi<**r<$8OuO2*w0$&+IpMYL`-l~09%hDF{=Qn7N8&9U5K-w^hGn)des zz7*kF*T}W1iEAu+w8}S^%EiYPu!zajL9JE888yeGYl6Alj`5R%2|}E;c24?sFX57M=Rw;Cd8ghYm06%s*e;h3$*!jc0+o(5;_YA%_M88{KJD zcX_dkBXP zKiIfsx>nn8c9+cj{7n|fV-HTJMtUO`7KuMx#y^@9lO-E6x-uyjRiQH})E(y6AP z*j>K_#u@Sp6c;Hw3hDJ{C9w8>WUNXp_5QN7XH%K__!ZVfJrV1ufZ0ls;8LUsVExyO=1$XUu+1R{50v^$ zG3wCr@mwfdFrSb`U6fBMmNz?HTV9I6yN{jEG&xJ0=sSpfEgZoKWep6JtMJm}-V^QI z8=qn2VbZKTolj-=M}d1^A!p2TWJXi@RlJYoWmK@wxtnF2&ZHUBkk`j~XIf7}sAs&d zPAA7pcQhvG#f-J50uS|DV8s>Ln3t#kdFj4#_5fSP?Zj6pz3Gy^4stn=Ie;?g^jb$e z&YC2vHfJ(aaydMW6BNZnCfcHMI*-=?0mX2GuYq-qT1pFJtDVaMZpGRq@XRBx|_@#c4Au4O^j5RU`W2aZX zNG4lx){Lj!85;+bWShM~q1b|5N~O(mV*4X;W7I+Vp8w^fmkw78lC9)qH5d(;0pDCC zP@G;JU5#rJEix)u;mew*WlAX~XQeFL#m{TsFBWL}wQBG+f)wYoh-A*n8bj$xuw@2y zSljurDabo^W3ex#o136!9q!pAu8F0!**P~CB1L>+sXyCVsmcyPayH7lZ9Ze}RJ)98 zbY=*q%3SlOo@}GVXe7^Vys|~;fKs&id@PoXmQ5|9MrFxvs+oiO^+7#Wv_8FMa}DvQ zrExiqu?*>dQMHezGmB*WiBrn9F>DY)`sz_Dge&>Qj6mWBPFAx*9<$P%RW2R4rk0-; zjjcqN=jf+9A(L4FyKi^6h!Zp)uFO(q{(ii@mMA60W(PMrJIp#--h$s#J@zM~4RK9& zj?_i;YcV{vO!AodrU7d^Ixl*ZEGf;E>~uM|?$3O-PVd#xmkOHKw*GC~6QUz)SnMS< z->~VBLPhV;tha0aLji2+U0RI9@_**kCKksx_!+WUtbmM}AlU%fF;UF_Ya)DXHPdU} znfy}+knUh{rC7bp)jwH<%9qHj=0i`m$d{Owo#l`t8``^@ZL;R31FA%Iq*hCmQZjK6 z(4hs7ZRig^L+i)pf(sGVsoi9Xvw^GNM5@q467pLk4DK3A$ocPke!paQQ}i|vzdI>= zvDVM^^ako@iU(yToQx~@#BFhs8|kIw)Taim2xs>=z=5jpF3PNTVLve@N$>p(BzzE) zayg)HStKvpRD=Bd4Sc0F9<_duNdL`F){^>Wlj=yy_)pV;CF-z#x3txw2Om*p%luHW zkl)U7&?Q*pJhL^e)<#)e<*IeM@TICbypZpe(Qk*!P0M9DUN^17U0r@4sRy3P?=->6>x@#99K1|MNYK^-x_k}}c z`d)YwT;zjQSTuOZL3RVLON}Diy>ECnP<7+GYToaqGrzI%QF!Un7eGn7x3-ypp&Jy= z4ANmoh00y<<@eH>Y-Pj+ln5BhjoD|7tLKlfe!%ZC{L$6=r-fxQ7eK}Kp}NrZ{Vo1D z*DqGu+a))u${|gAZgYe^O%Q-5dpt@2r}o;aF%zg$yIb3l_l(_1>0LjfM+2lok|%0W z6vkY9Xod40N4J)?HFEeJ$uKK}7!pL2RD}fH=E_B9DG^H=5rc6$q2;jix_67w5%CU7 zd1`>Tww+zrF)N(}bxP_==M-zWTOmt|dk)N<XYJVCJxpU+a z-ywG+Bt_VLUSIn#H@I*8l*O=C2zKF#Jor zKVUWhuxJ3968ensX8hyCKYPB6$K|%JrT0W!PMo`iD2Cf!c?$`lMa`(1?HKKNwoDFnM?Uf7qGSU7QnJPn#Yi6E4Eu1fZ+C!t4@(NVN+Ov^(`oz1KsK5-~ z>hH7u`ic6y?SHwPcHq<4sli|k=cw!>OlDL4^?!|)BYOchS@QESh_S<#?GRUGdNp_uwqAJe$K2&nklFYmxzYa9MazFeX+G=N28b>WQuST+S zGlx$#O&+3D<)KKL7q|ZD_<8znWilXg9D>Nl`O2LryRFe+D_(LMP{{`zYJhH463Vm};Y86-EajkjHjL zoD)5eS$*tX(~70WpPDz+pJ@>|N5Ke({4`>ReYj)d{KZ3_a2!on_S)t?j#}FA=dThq z_zi^*eXl;Xr%iTr#HHx;vov>Lgxp=V7M*T{<=H<~D-v+u=Xzb9Oo@Ki%R#+5KizN%3*8KLnjz!95` z$g~n%M51d{9WvP0o*%+K%)G-P=x2h^I@b`0DWHE2@IlU)T?<{Xy0uYg@FtXB67o)#T;BZ9Y##I4y4z6P^TY=VX&r6lktRrnc#Dx@So&w3%Ekgp2`M{r)V;!rN+~}#>G438#6!>wUkvGB3>bQ9NUIVVat=Wn5-f5Ev2F{-1PlkIZk1jo9oX< zxbAjZXGy3vajgLq3-I@S7Sg7s4p_mtPUmM)9YwNmXxEnSd#EYbYBpH*9Z zCoIc~-KP40I9m5d^M5ENNt97Tg&xZK62qxZ3v4^`Cz4Vfi&8nM6YLDN_3#)Cu8Y#b zX(w}mwS_xv@Dh`g=5fmM1WGf{(NU z>>v?7$NHgt%Moo=u?i0@c=LIdbyMl$Q%5aWYl4IVghRbLO;Q_K-!(qiYV*j96MqtA zGm^{0li?I{!Hor)Prgi&X;1@>yzQa5tvqI(Z_JUH@;6EGDGc#`T%}aom$F&JJKMA6 z%Pi_%fRC*mkQTso&HDBA&*YbsE2CkxTD)pizg>BUtlWp03F0iF;q@L`8g|TDAL9bW zpWxn|4j+o`>6f~Tgat>JErs{NRLRLY_H07HIs9s4QD5o>W|wmI(&bOgT9RdqU4-aW zGJeLqMGW#WgztM!MIzx?Ka8}7PyU@KD!{BJYt6jEyKghS@+H5w*Xb?nbTMr|L*Lp}xli-e6BC*VaarvQkgFJoN@mq4YuW%{!&z-2kB(h}F zi}Gi6UjukWZsR{@^U$)Lr;M*I2r*gtw~@>z6XLI#v7FQRM)8RC5BhS(qoZO(`%aNq z+SLN9opcq8pM_wLoD)54{%I;LK5l&lL_RWYd#EX9A5{_dUNP5|RkkiAqiu;9vVx_S zOKC8&j>5&qo|zJ7*D7wYHA}W4UA}3SiJAPUn24x6PnNiBSg2s`3!d>ak?>~l)gA-Y zz~S#jP}4nTYx0D(M+`@fNUz8OU(XcJxgbyF)@9tCLH}thO6;Q-j!8i=F~SREM;|jY zy~?Vg$D5{O>B-jq>lPIPeD$Om{~rng7JBAK{L~5p;m{(=Sll{VvRDBNq?oT3M*w48+I6{l@4d+xai4(=~l8Yn>8D?hY4`=+qw9|vzpKzD4&9gOd} z1ZYfT6t=iGi$P5hN#}Q_x zT<9kAkNre@q-Oeh;;3fz@#@8MiRV^eAXE#_%91&PkK-eg%}nP~*YCLMPgU_FaUht- z&80H1mGjs8Zmo^wmZS?7>O+b?Hg{1vC_lY7)DBJf?6E>-D>@Z@3FF(kLyP1zeDoot zopG2nX7Rg<$CYC;?&$5kXexmb%cCtO6>zk1L1il|8z(z-{iO%VErhNad9K~R==ER4 zGDBEXgA~2mO;P3#GkST+6wdM%a$;CstbJdq)9sS<;=bFH?RZfiM2oiyvEd^Z2^{bdvRHi=M{OLv^H5oCU6JcdyT=9A?+;n4gRVX z6c*hYmCIQ@j{H;IlkScAAF{1 zMvU$dK?&&`4Wql1ZV`#mNH<7K0SW1p-=F72JTKy$&wbAQ{azQ-W=i?0;!Y?jaZE&l zbal3TcaZW`KQ)KLPUZ$vp0%m5Km~-fTk&x<#xKL66=W*6ZPOkNOfzQ2n?gM?V!1}c zZ0${Ep8)?-If*{f7($1>ai^^yi656xx5Ziyk-k5|-qQ9WWh-8yDv=L$- zdRX6W4`-5@^Lqzk?2A28DQW*cx5b-iW7nsx=2z)4@!HJF?Kg%`SclphNM4cMNIli| zFEwl{ElWrrCsa=#XG9>LIiG1jTMXT{&f>m#rTsGgj>8zZQ9f*V)X4AUG;h}&o)w4p zrDVWnGITyJFR#qr8o#jfa43To?g1{R9*3;Oalz_H8XAn6Nenr&UN{*RT*G)^txDfy zpo}5FAa_r@F*uSskIPT|usI|9GiKQ%VO|NDO#=W&UC~|qg;z9<E#Jb2418vBgjLq?2eG zwhCP+eoZF3cTKEkZ1BKz#N%y{^67zBjr~P^!6_%&M3Hlc{7U@392SpYP=4U8n-N`2 zMv-uGaS9+y(w{=c-Yr}aK+;b$Jst+#;`s2`$wB+Mpx1#@lXpcnSBB%H;5j`yt@F4> zP66Ga)(hb`+#+7V54yC3(aMezWf|-ktQ+<6n5Z0~AN4#V5WE6ig-E!I zT}5qGZ{{YYW!bi%DV+C>pku|x>?aGlSJ_S0Lyfc=1O)>rq)@*oVCA9s(K}a7|5e_i z^gG3G8gho(U(H}t3aQ(LJBjC0V-v5bBja?Hd1^Z;OabqCbIsH(DGzwB^4@dNM40fC z4Gyv_{Mv`reCMZA74y_+cXYVQa-TmIfTp>9e@P}DoFGe8HLhsh<@c}Cmiuslz2?`v zwcW2O3YQr+h6XaN$4FF*lC5~=foAethGSD^&g?TbO2W|8YCZ&rRG2%Z(*oZP%poETK2p>=k?du~! z(*7S-?j#@4I+O6asH3jKd0F+7el&q{(B@G_2q$5x2FBl>qO>@wm)sg#ZTQ9~=;0ZD(DVY=$?#KsqttYd3qfVk1g^AwOKgq=I@P zp1qV&fQ;RRW-2KTr}FYRzAwUSP(Z~)_{?q9)VX#*5^tzq{n*W1&)|F0!?_b=YxY~` zteS<2kjr-p#F2sk$D6^r63u+to^>=^q@#b# z_YTOQV3wMY-!|+R81ET>9YGX>c16_eJlM_JZ*D zonk#ZG;-y_N<#r*Y=F@_5N=T38M`5ODH&tictrzbhiI-~P7(;YeAYlfiCC4C{7zk~ z`STKrQ>B{o9v14-gU_zbd=a>N!(wo6JK5#uimz>A!ng~vky3(gCGS=T&#f(^J0m@_ z(>trcYK9u1Z4$W?qi6MM5L%c2u=?GddPBZi*25K2yU)7lq4aOA4REt``QsI5@rYtKg@vh)jL$WcWGDcGen(_{=|I-#x z1> z-|tHN$i9gE1M5l?ej;Zs${-v=*p=Q9`LE@+)1dsFLuU%tj_U|vKl!&4YTZ*Ni7&48ba_N7PnH(I~-E%4bm}c zpY_Q*K5;#baB6`fQT?6~k>S7YIszMH{|4(LHoUA{HaK}nFK}*pbxlLnxKxwA;IP?K z`!5g#l7~_0^(KE7L(Zo4&-)o-{;-DcbIH=iwp(GT<(clr18%Fq6kW&AIPzf2p7nn+3+3ltrIEGsj&#HtjdiDDreJ#_OQM-I zMlar$L~Ys9`$VS-;|)7GC~dt>K6d=zs0H{t)u#l?2pazMF<=uI^c zf!pKqtOioZ(heOMzoGu?x3Y#{qnZB9yK4LWdw}3Lv~G&5tu4PbSCN5yI@WL*ce$7m zGC-UgOD|>cPlihD|t!B;tyxj`+G<#Eea9zHJz18NZPK{D==q1IV;4n?+JsTi`{j7))A+RNXqvrW*k zFcQop?0A)PK8A;$$G4`x<*l_85y19(R!qgv&;iY(fRUec>bCMzfn z&-d1!ti!X8Tu0CSba74<$)*P%Pbq+}W#xM7CZtqUW+s76n)uTk3bvatt_bOOl}Me6 zHrhvY2=WJ1m?ktM7$2Vj6mg?ZJ@Lib#u0^Fld##Gofno%&^}(JRF3HYh$egM%eUB} zS!CwS`ntn@%#P*0fjWNc8X9MgX?CSq%<;1&0R%-dt-nA*Ja#2yI^K2?a0j1n6@6ln zFgLv6SM_yO0e=tyCgzMqICDZLDs{3pdo`I>2nn)I#LD8rhL#h#IGuinlnMI+>xRZwAw; z;I`rK7qqczh?axGo3CER18mmK)mc)NvpL%_(-THU`Nlp&W>-ofXmS6HeeX|wtnwIG z+TZ;iwLJ|=!+MtDvkEm$szXk)_PsVb$|36eOV<6##pGI>YVE~>S)ZE-VF z`ya0j!jYlh;Wp9`Y&HB|Ja9hQP+2jfRXeLAAdWTr|Y@tLqX9 zW~JyTpP0oN!PpL`_bCGmCJ1%K?`=`W78-8G5yB!6&620zIt-L0)hni+&a9V_=#j!+ zCymu4HiFnbVkxxFsqTCA5e;*7Z)@$>nk>*o)8xlyu4GNkBBmm&YpJg9U-L>I{QIqc zSgfGtDce8qO!cdpT-X?+56gKjL?CPO{Ecu+b^XJr&_m(X?RbbQJG~y9xD{M1|5YtU zuMoEYQF`2&_+dbhoj}8la)A^69hN;_On?8nb;o}2Ev1c*xppQRr5bngI^}%fup#aD71-8j0gh&8l+YMP_*^W z#cA^0%3!8JYai`h=c!!ikgHJtLKC-2619NIl)J#s7fiN|_0}z>Sv z=8dE}@wN51Z}RCyYh>vAlHi_x$f#IoFG^@I?4vJXEa!1lTu}Fx|Y4H-ks~j>qn3(@%>j7bz|I90#w+`?(yDr>diCNmo4| zx~+2tb*{06kn6D;tJgBFwBqdfJq2+fp39j@Y-*>ly$MJ?XY>?Z?&1Ar@y04bJPeCI zrVb}1l(wP82sgKS1SCH?ebkr$i1{e=UK?h&)Sx;9NolY}i@)8xCZ_7qYX9-D%XP9g zyuG)(&E^u5ZORoN=OJ6vY}d?3j~P=L;dj*NuJ|L2uZ>uVcN~I3lB!C5x&Kjp|3Wg4 zkeq4VhuSW*PR8z&ymy(z+TwqgMh>=RtIqdErGBay^4N~bMV7XQDW`DVZ%O+N*5d>} zoZ-mE4ktmFE|vP!<;cOOl;>jLQzeeNOzU~Z*OagC`+mb~h=2*~=G&lCau!yCIWi&Z ztKY>Y1O7@|*;@5Gpz!z)go;^M&gqQ)C=iz zZq3JwczY)bH%oIY-9z?ecvPE40H;{Ftc?BrIA0E>%50CsPFlBLbZF35@=38gzY|Wu zEhS1KizXrd@nc?Z^r-}PFF|&l*93HG_R9Qsg5cY7{#wQ_UhJpRijivSWSvI~y|?#1Z>4McSehnMFGW ztF|POEm8<|sKE0?Qoy`Zpe6#iB&tLE4b*gz;C}4IhVO~GpK_QeRS8t5FQ)|;sd4c* zN07x3k3**I6JTNk5d{WMQSdj@L z=2bip`Di44!1U6ZiR@@cTUPDO%7CHU{J1RD%qpfXBMX`8Z?G@{31+bX>?{61Z z=ayx1ir6xPmXa**hOPcr&DY?RB15p>W&BThTv~(we=5V zX(N+Ch#9VgJgDKx*fy92mf9}()q-yAXZFgdZnaoAc8x}B~ zMk7gZe_+o#?p)jouDUWa`|;#IYbFXm3B5|Xc9p}>?ih?k@{lad*b_euB1}m<$i=-} zsOG=Sr3a2i+Meit7>d$gA%%f0N5cf z+{Ziw0onVfkguxqA(nw*0aKMLr5P%Qh^8o7_}WQkEcB9$&G7i7&l(67s$^GB|1`&D zSEAv~3!MmOsv*^EBNn)$6K~N(M*lI+D)nY#f533}aE8~YI+XWSv9A_)K}(w9uDho{nx8<0mGU2eXqSIJfqX{Y(L84p0s#WEf=E9~oQukAzZ-%V&ddwT4Zn*z+ay;L( zb(Wi6YxGZ#f}8 z?<1ktIBglK)}1CNu0$`l*+~0T9L!-b`G;IGF16uZbbp5$Oq}ISG$v?jsqjr2{Ldhw zLX;25lI!2;xuc0rHD;V+()1d&8XJ&eDLUDzxR1=jL5oLoXSjapz~ff;*5~~Xi#EhK zlvQcEm<=|M&Z&*3WFFM{WhDowRa3zUYYW_h70;BLABlGha?G#I_g|(KEg!;Vn(HL} zr&OvCO2)`?u839b06BVzwr?9;w1Z+eeOk4%LwAURxSh#J_Et2A9%urtZBUHxEc_d- z_&xJCiD4Fby9N(@P}2W2Hw=f+5>O^-dT`vfjG9bB88j1{g%Wghj%04H&J=lnY7)}v z<(Pp*ztQnMaX;64pi!m`2zPo5A&~zjsgrO*iqt6A)2N4@ysq|w;a=cM5g=WtpIbFfCU@^L6n8xo~6)A z`kM|yuEU!b2N*gDw7Kuy2zLALD`&yKqSFksQT_YD^Fuy$)N4@HWP*%+juc+723aio zr;+fK=~u0{w3QQ>ptKO`qX+fMGX|`oILwf~U^7L8q5c!{l)35QNck>iuX)JHt0FT>*ydiH*o#|NdT2h@Mi1}jWF0Dg`7OTtQMt~>Ymuy1 zQG3digN;)rQP;_W+mKNIF;ETk$T=&tf}JWiOGoQ9 zNHD^0>~l$$LdmKnoM)JX^}-dS9SW#0yE;<@?_JcGGQmBL19v&rXWbOZlU_8ljFkOz z9H8HFI=uUp%*2dXd0*CbxcJBQj_!j{y?d2F552}g!I8+5cPa{y(7?q|&m?`nrB;Bo zg}v1Wbn?3pkNL3K=N^F0L8DXBrUK;VHv~7rTDy|Ri1Ub?st-@93li_H_VX_~a58v= zpx0J53ZsqkWZR!e!E>T&LhaUl?>J0f;#fqaul3L{bKGmX6;sms2!=u?UYl4$7y3~* zSE@8P*$2TM4KB(&F+lm_7kZBp`oe%ioNTc|T5XnftPU-HhQ#rYvUN1gY8SP(&VQT_ z;=X+G;g7*mzGKj%r7)a5D)?5Fon`R8T+skt3=XX{gaKPnoO(Zn_lw_>yx@N1f}>zG z;aUSi*|^P{98-ZpS1aNbk}f`uqy8U=8+8UsxoiU}PF+ahY1||OiEOUvs`_&&RbNA$ zxQX%MY3cY>dnQxENmTdcfAY#7o^lB@KI6WCOxBPBC~xRM>YRpxt@)S--5_v zOzIAYcJhQXu0JvbU>8Rs%a-}+WkHTTM`ZTAvSEn0U8=H2lEWCAeAfbOuDs3GmN6}# zDpzcbBZn@CA_XscRS?(l8;H?ytgaeQ{{m3oKPX)oHQRiBDp8+$r|G{K|^1qW&QHp{eQm0rqZp&N#c(A4{&1Pc@S(PJD$m z%_r*GmWzUq@(HK+eo81uYufVFA6H|hhJgcQSdIK_ z$?g`kb=_A#rwB-w?AiW3onxSyT+)1njmFjt-;0@vr}vR3#B?omN|R}tT*6pc^O-W2 zTZl-!kH2NS9C@d=T=YTn*Y9TkDMnF}d`0{Pgx&GziX)Bj-=WBO>6xtZ*KkE}7*PU4NdcI#t{ ztAV|iA7+;p*|cX9>uaivXOzN?^qrk^Ft$toNS28?!H;TM>JJ^p9-!CPGAlv*g-Md*!4+#+ z9ucS04o{mYZtz1w$Mi;WgSRNP9qO0UkU?+nmW4}JpkMV$z8GVwp=^ZN-CVO`ki}-c zM(%J(rtquxwQVGEL6XHzk4R~YT#KyJkA4W# z)&{w2VYA~R#F$trBa>~`B~8mn^4ndZGr51gT+VRvpHS<}`tXAngEt%Hm`8(!RjB5s zgb7L-xK{<_QeHw@A6!xtd9xDT1#fVPg7q4gQo*Qi=vi27E647>6z4M}51|c>5G3L% zUbJ*oX*laTdYixLP=3rzi`;DY`_ZH&=}iCei)Ls{L;jtqE6mHbtg`NxO)g`ab`u{>P-E%j?-vRk`-&@eNpqp zk*mX_X(g3!-2Ri}Ux`mILe}nKUwW$r_bdlFl{=CJsd)SJh+bhlpqcBn+%$Jbvx$hN zu?@xsihHb4+A*$W+(R}qOA9JAW zFc?{V)PcThd)K!?g~pj`EADDurxSJA2P)lx47|!@%I-mhXyqcI!eCEr+fkuCvVL~Q zgegP^y7V`}o}&Cv##S;@IQmWXot^iuHyA`4M5pRRskY#?64tXChBZdQy32$j&!?#? zUd3#`ny$IA*ve==Nid;d&jX zRTHhE;8&%2<$-hXD?Cr4=-O-O`zv6gO)t>Vezb{ir%EFOT;#p>)e*5fFU2F5VVG$o z)g0jkSQ4^(k;j=~t40N|9Tt1Z6TAi~AtjEqwmJb!94@_9WGVd|VK2Vim+k*8%^{t1 zj`fQ619xC0&?vH7aVJFENqkIKKl~YUk)i^eL`&9B-x>TLR(zTU|qx&2KLJ0#Ga zwPX&|xOyKj$h_tmUUg7+yypb`RF6>{PGqT_%RE|@EBe>j842E3T$rS$oR}^T#Oxf) zKiUsNbucltFuT&HWt3@|OI`(wKq0sCL;~SLVKS%Ge*Y?kEbW)tvd(orv1O1zR5rX1 zNy|RdY26~1*<9GB5G1L?{dHQu%9DF21sXsgKN#{VW++Vj6%V0=*bVB*<8D8>gRMgl zbBW?=HWh+fD0*lFxXS%G3`(YT#p>Oe*Zp_NZ`n&k;w& zY|}V-KT_z~Kx$cbZQp2&rn>jv|FG1uckIL<4(V>Bt@%Klubh)L%PO3ahc`UQg=CrB z-lj8Ql>+jDqj@Myv_BQDQj(BtTC9@MDiv!b`}T+@ltSl+)&P|-{F?%W%J3y=GiLxA1(!PG&44t>fe(ECF)I6g=n{ zsJ$W6{b!tVi%y}KQi|IE%kkbzdT0OGID4TAQB?2OCN-_rGXPD^DZ=VO!$84nu_232 zTOFI7aQ0ufzPIHx^L1x6fG{!t&#!E0-+Ho2ZQE^Lvf{t3lzNhdjQ(%p6k^dlN?k_u zpjfN8gkknd&eP<{?YJmN&|{P{FJSH)hRT$Pxmi6zU2%%Mdx@u}gbHS70cY{J^gyY7 z&D*~5%2`3#v&<7cd=_R_K~4ps*XxvqQ(^(g9P(x(vue#9vt zH?r=ocVRUi4zw;adCFy+7kI$x-KFvlj#5gcBmq{38n#BDDa43WPDsc-4XpiTr=k!} z)6uPuPPZX|^QRlv9S6%^kIiwC7&iM!cGmsG0^P>d`0j0|Oebt|M#(s?e`oF=8_X=N z@}TCVs&;5X@eatP8#6}~)w6fR^`FLhgksz+UP@J`2)N4Zo^cZkrudAZ#WI;xkuERE zu&;cM(KJCCHl&8y5l`HYzgq0bmNGpM(WZ+pmzGeULS2MVb$heCfzJU3rTQ}{7}lZX z3KuyKd7j!yLrUd?4XtjG_Y_gx%u`Fy>!&ZtdR}UriT@yjY9|w)NhlY+^qHxV*K8L= zcl?m(y2|`q(CoaQJWijRdm)p;fT+2R$r72f8Qd}dVEdMwLEu{OPgSFbATn9dR`{7pXSyct*eg!w<*2PY|v=Cuda#2&{NTk5Rjc^0F<7UgY{|k>kp#mUW}0}lS@RB zb>6}cM}Gc1wewEozSyHxQ~BjpZAr>yKfxGjppI9;D_b~<3eRT1^G*=`gAw#XQu#L@ zXi6{MBmEJtHBzQ0$DW%ZRf@9j3u6I)*Bv2>V*B#S?%=GdoTK$u5C13oA9@|m=tO3x zJSFWYy92BVMv{(j%$)-=rJaDrQil75K4&h_xkYh;b>Ph^@i3w6A|x)!N)+9cUrQPU5X3xsit1Deal*b%Z#8{m#|R`#q^M|H}dL?R8X zvE(a?^-A=Av;-&2M|-FmvVb6svSZUN82&3p1MzwuLVQ-%WjZ|H&p;r}`c2tw6?WYp zwYWf|@pKZ{zvfrZM0B1jy^f@7TMJ6AZty^WK|F;>nd9bP10sf}g((JCmF<`bPM;@E ztz?cR34*dxZ@oZo=0C=+BrvD;@0#`;zw+4l#hfYlReQ&!aWK1?>qV6j_TebGhAxh} zye!|^iDxG_C((PqgC&49f0g@6xnqKo1I{C}ZP8sx`4@dMK@#D|Z;!PPadAd$O97dxhhtnY z0x2-webvQcuU-}`u3jO&0@Np)k@ zGe~##mmDix%hWJrCeJQw%CF55P@;)Em4ma(GZrgX+zk!`2g;}Ifw4zY^did8;CcLW zxT}chOhtWus^!7+p>;I^%wWRSJ|xCIe!*6$Bt5asd_X%!CLZ5l8oX3ZD-2{i)O2ZB=G)w+Q5s8nx^Bp@%)|uW@M+S=TZoC- z1Es<&PWas@l^iCE(U%jiwLc^&Z3E4+y$f%&!t3_i@HX`1RWxZYNC~WU9;+R|x!gu- zf_MIzJ2|2luN1sE|G(@>(?KmvP*L*VLbc4sh(@!Kj*q6Fx%?!c2bRYSwA}}Gu?wGA zbZj*;U)Sl>3d>;{>UVyO$7c@28^+|iTW)U;TnR6I829{_I!{4vqj2ky%wx7-Pg0cT zmu!TSuno&3F1oLLIqNtL=@hhPLNe76VFqE&i~L7E4xjIhX2TIKMS%U}37#Yk*8i|n zh7I4B|6IpQEIl(zWpZvNzcbFtA#jF9T@5AgZj8_lv&mI`S^Wpm_KV5QCmPdsEzYvZ^i(m9Fg#lu)Sw& zl8g04DQGO*>8Ka}liCorF8hef`@`bd`IlV8NM4?6p{;A&)lsU?hCr$Oz3Ny7aIhpH z(cc1hmJ)bg<`=F2^~;Qrm%joOQ`?8585xonp*5Lj7*D;o%+Ebm$BNq ztOa~@Vg)KjCOU2o%c=@IoNf~Tj7BL+jO!09@Q>JEs^umaD9`6$TT3aTJGi+t>_2Ty zk-cCzrh&U(A{OTKYA#Tk{yAgHF|MXB;c=WdpLIgVMT$;&dzh`?UK^ekwOwkeQVf;j^ zc3Xv<>bX5SZoj)k)A49dwoY27NKY`?6?%Ez(l>$x`1y5Sq&F`X3k8d$ST=pr!)+)o z7R?Uo0C)-qfmrd!!U*eJO#aQEFlDP5xeSdH>#FSA zaYl?HfA%?l^4>B`pk?@0U06ocW%nJ83A57D!AnU$sJW*Vr}hgRQYuyQgiP z$Ix!kBJqYTy#AmE^$n>S!W>l20Z;E z=7M#qN|h~cT8(*}IHu;Ed5+NN5DbQ*(}$;}Ey?fax<#s$X`VK8zUQ7qz2Wa=US^TN zP2-M@0EK&*f?K5AG{urtz2=j!RU`zp@P_B7vpP1rSfmWd0(N z_L{xec8||5tB=Fa7G!3@nc>gCcZa>})t{|fShP^B4x}&YtIGqS_3;-+2+&+HHI6on^K0{BERTZ!-ZV{j^yGPcauA2RB*#@o(>g41SN!GWJHEa z<6_A9rY(aoLBX~mTC98B0aTrBIel(chTjkUW4LE4p|N@tL4U%tNj$TXWY6Rt6PJOP zblP}b|7OlVQVL<9wf?qMTq_4 z`yQ1@_NruAS6+TtOW!oE4Zjg43wsLOzh0Bu?zK%S3(CYufR9skLOP0XoI zd^tbUu+oeE5?Z?48rq!T(GO+XvYW0>Lcc~-?Skt_>S{_c8f8TzB7cw$7w=LuFg~_e z)i3!wu4~clTuv`t|7&*?g!ff|_XaCqz<5 z^Y87{e6iAl5n`X72W8_PyI;X}Jb5tVMiZYDQtyMqWLJFC@Tjy39@$I+T9$E(2|Q7D zFu;FU2SzwKO2bf!ui0ja8nI?0F4pvv`;?n>BQYV}kmgKj)eCk#(k@bp;f*?(>Eo9F zungapS#Te+hlydF0@kj<9Pj}*r3e`tg);$fxVyl zgB`wo>u`IQ2zY7UD^i+0pJNjUi|B2R6>_03;`-QPgWbuXlk$`)-6`*6e`sO(59?&(-y8F;+p`U`{DpEEU76~T`i&y~ znXB#)t^9Z00a7$8U+2S4u7lIit>k&TR~sE5?VXjChrvtDveS0pe^>+W(UZYajpvmcK*HOP7c=gqWKKb*HnWUr%gBtNK zi5K@hzw#d<>(VwXzb^fI{4@e>)aH9UG>5?Gk(1{kGZbI`83)H7hS-jGL)O=K0{IKQ z0%#w>#_!`ULxemCZxc&Sswe*)cXsRRrh)t}%;iUdG&HOF-Tgx@Gw$DoT}zy!Z!Pu$ z2;8>#gUATRzr?UKax-bb2#Z&uPxFF4piVcwZF~oXME7VV&PrW^WQy*D?EF@0*+R;A zm%g2jKjexMU%qoV5$kegQV5f#ZM)~-X*16n+&(Gp|LK0+XeM@ce$$swMI`oHNm0pY z^BGerLH=8XE0DlWAu^xUPwJORRMbk^S4I}3pN5Okj_R#{k$}mH?MJaYC645e1BxiQ z60O0JkYp0oU+@EMq9yCHa0{b>6!jU|(`Eyngp)M97D|54hR)Cpqw;dG26z}KK}{}( zby%8?pE_xYEf|i6qgRzkPrJTlf8*FkIaR#@n#sc8s^BgqNJL2dTR#5_g)7;Z!*Klz z?MJ96(j%o61@%g0kPCJ3TM^P(=M9pc6f^bjs{jg&`Y#V?OeqEpO*42$0E#=K3Nh}v zBad)7A>DPGR;q;jIL<1HK35XpOE1DoGVo_R=z`wdL0PW1F1F>TGPtTuB@XVXEt5jc z^-1oqq~&%NN_=t|VXNoYcua1#HGk}5l$vH?@Rm~~MlSC}E&qVJ#K>GXh$a`1to6f!quGRoYC?w>Y|nUNSPm zhmEv;z%I26Vk5xop+DP%6G)ZZCT*5l2Iu(w<}q5qgToh)h|2O|1_G;`XRgZewI3G zAdAv;IITA`8Dc%uyMK@kT8?>1w8(9vn`A?s>qI)fisBSw*-(@LgB}-`qr!1HtoBN+Wv>m7b0?7H>btY52vHYZ_A7*43 zdKW=xK*cq5Yvc(}`TNg+-0a|UWwNCt8&f!+!awa?AJj)gJP;i7%%C8yf20GS8>?!V zd_9ljxz-HA^{}RoYCxcY#gkk01+^zpmS0pF(QC&{4IC|IMH^lym5-sJy@;+1gjpLn zVC4J88PGLY>Ps3E!Py+cVb}niOCR}|9)3Bi?_C^0{t9iUD_}6Zvn+sZG@R1T-S9W7 zCj~Ai580*;_e25;Cat=0DSrySGyLWeHpqAhtlih6iphM*pJ|d7}5HM)ept7gY8XXE$u ze04t5QR-N$@o*?Fe^dzJ{z5_gWa(rtAPh6KZVX@+OaBJ#nUk( z+H5URN7YusxqG54ibSnftW=pHjOEoBf(Qk{p98K*Y+`wuQ<{Ux{8Z-mUKQC^mRS#9f*J?R!S1?+OaE5lJU}B-C!&vx2r(q&I{8 z>`&)`glLK)%m$L|K&k?zG?%;2}hr_aHwouat0?51HSv8}dx%Hd>VBr;+|iLvcp z-*?Ld=$TG~Y`1lD@tXzR`k!0p;t#9gi_1US998#BDd@d8`^1^+J5h7y)q}hUh^|hjRnhcLlB96LI@)YMd5*T*Edf#d!a;t679_2R;`u#Z+!4Ei)S>>b%u>kS+~%QO!`EG5N^KP9Vo z*OrWy`BjLsa!Xu>pXt5!%Ng;pdt!vt?zuR1Mi_mT+Odioaj%IX`Du_{C73s152rI0HPgZOMS`DE~VZ1a=aa zM~Z@2meD(GrXs|CJVBS-{&fFn^NROPy_AVlbEO&kfju_M~^_JG&&KgLNtpLSiJ z`|{WP@)3S*G$h$;uaA*@cqg*(0GbPX8cBJwjmclR=U@AqK@AvsnK%~9QqPB6AxeE-{#}@vW!>`3s(~%;EAIRJFO&eivVnc^MXDdev?xe{{sza! z6cfNsVmRuTlm3F8^sjw&W_4{}MjcHgIL_;0-KCaYk&R# zo16M&ku0mF^N+Fyd&R)gl8E||CC7TSu}%m<;`T*}-Q~&SM)^K5wSU?XT)_VEQE231 z^qP4#dZB#cNg{PzsqMFN?=!6LXQ{it`a&2XO&)p(3+`6(f`+-biK|W9MOTfif-2Kq zTs3xKdbl@|t+I{nr^0V-zqX{2>7SLmM7^UbiXNm}d%`sP51m_f8}TZuu`YjO_Y1~7!BHDY zP2KDwlF@zgA6D9#BRF9pjoL#&APo$vvQMpS-EOOmW%9Xb53|CwX=*algwqnL$?vboj@><7X)E>UrC?+)T_6s|o zdw@W2=~($u`)xIP7R9ap7b#EW=sl)nIJ*0B2tdRC3t(d`zWtbN+f#HptgUF+<0Qf< zrrO)MHItdt&C)>bQD zXF0VijTFTy6PCL=7&s-$GemG4cy*NvfZ;!CR>kl{gE|HC5HC&em`Q#LcV&Iqe!*@e zu+3_n(^7;^zqhAKttJ{61GWm=&r^^9C`YNj-1!HaZNWq-XTcJo{y4={b9gFT4_o3gwN^?|c%*}9UH6y@ zMAM=IzVl)$S|Ek#nz1^k=l!;iiY8AsyT2vT-;u>#3wSGZysrc_~aRPgQDjLawpla&@1$w(wo_?haX#3=~$8036uAGcMdpnfDJ*fK>U!pZOz zRd*{{X9@^Ql~9Q~tt$mOmmPnL>I9dng9O;>e3zXG29bWIj09Mi5pjQuRuC`Ss_Bk> z&s{w$_5853^&r=&dvaW~M2LX+O66wNH*q9;JXLx}p|P-7QM4Q`NyFtcPxFqhMHxPw zS!Rt&blk6sF`=(j-2r0d-y_gdIc@d{OhI-^H(XQ&7{b+*?X96seB<@?bKFyU4w+eH%Up-^ zebQe>480!~g5g*qb8HT!dtBQNrG}x6JH+H-?}o)(I0<0MW9K^`4Opd44~+e@4)Sbx zE=mZso~4E!m!vJ!cfQ`L)hF2Z*7tq=sGP_%(V#^Vg&X?`5(&?8jyeU%Bdcgk+!c+K zOk(x|%!SUDxYnPg!Q7(I;OMv@{9#qk=2Y3$_{<6+DeNhIC@TO^E7-PLgdf-Zl*BBC z%cmLD%iV{3*@ow3n-G5Nxmwb8Slq+U>+hb)ne-(IpaDJR95@ur4PO2qYv;iY*Y|gE z5uHS@(R=T`hUlF!dhfk=QV@NNHki>nGx{LWBg!yp^csRe)C3_U;`jHwhvyaCbJtz# zoO8c>@6Y~MQ!1~k_C`GFvjUl)Oh^^jS=SU4pAbQ9$k_QzGWLlb>ie2cY_Nem^sqyFTJDD8J0>LUzxt|55`j0;{6F~@b6Tar?@uDnH%RTX*iF^R6|O-mWp9*aNyD`TYbH3QHQ;6;rk#r_kx!18AmVU3^{i>i?x*drbB8XGkBr zi3rQ(ot%mD!}@y{i&LghHNa57-w)L_oK1AB=Pxd)L?WrmM!1nwV+A${-=@~%FVcog zRV0GY%l!}f3UM3NZK<(`kDNI%{V~&0(!lD|+@q|EQ~)&0-WA()q~G7845B}3>yP8| z$UypBE%3$@hL$6&uQP)l=U{_*|HG2Cog+8_vaX5t7q-sIMY+t1Wovqw%1GDyRO^;5 zxX65>f$hh5JJS}5yA)fo_pXFA@Lw!C^heTM*6#{Prlv&IS+jH6S!j8bhXLRMu!0)h z54e2Nbis7oUqKS9r|4XI0Zv!Ynv4cOVzs{GbJnT8A>SDaBj~OpDk&Huh#oJ%p^~cY zO+8DK`w(D9UryaPT0V;2PGh3Ym}2|B$R<07NMaS(854R#T*2z{P4tDbjPrLl7pl+V zsa)PhUSdrlCHLw^{$l;=OlIGHlQHYXfS^rckKvCRLuQpr*Rh<~rY&lZv+m7V&L^VF zp>{cspUJh-3uV}uQ^Dc3LR?JN_*Ybx_&4(_u@oA=tZ@~w$a9hzs?(kEdbl8ab7^F~gFRYuFRnV=WWVDLG%b_V?q;k*Tw$i;oY!et2vE)D{ z6rGq$#c)0n%zC97Gc+ScW|dOQ!@!aqbcT-V`dSo~h0v7Ux{g;c1;B2EcGE|z(;pUL z(KuexjE08IJDBm5bhLUQKhN2p!4=N4s`K$Tc?zRt)p@mxesikt-_ob8|8(#luK?Ir z^EUb*Kvnf7bum7XS|x3M9+VwA)#^))GIx_p?*iORA+gu6?15&fu|NTk}@k(~9-D{$wNA#q9@JtC)Nd|jb@(M`0 zIko#&LwlfmFA~KvU$jF_Ds}d&q|OWIEPSQ}TvjTJ)Q+0yB2y87pE|vP7k4iX!dF^B`CG9PKZwBg+gX?%!N&Ax!O&X}lrie023H zZ-r-SwF{_0aZE2_@61p+>EG{96Lb>9E;k8d)zv1I7J`-7>F`3kd>@R!gA>`Z1IorWHB(nLJbfk5hA zz@^$_2&j)uY_F0|YU|=~+&VYP1ES8s*|ZS%bB>Dwky8rXd={h2HdLzO;9!kG-XGgw zdsvYwM)8nPj!gC6PxITd5hr@@V)0)DM;s?fW$&T0bryhNF zD2Y-UEwJ`eAkKfUyGK@M0uS~j8Ma$QC)e<4aCrEbY#O-JRI`z6JYQMlGWScu_h53E z9VRbi7e~EH=G+~RSTeQ!_|TrBrYWNXpIclO*o5akm%3dnQG%;PW+u#jn5tg}FUI;t z>zRZ!>2dgOuh-Dxh_KGT4qSU-bK8>c99@^&=22`2%t@f$w2V|!;v&>bXfi!zF3iIn zPpPmctL)ZEd_Ov{o^tzhb1X9T?@33U#0yg^KV#WaNeMI+k&rz0R6S49i!3@kN9~vI zzMWbIl!FTuVtY zC83E@my#CZ$&Ku}5vrlDZtzXQRm&QGmj~YRZ&XeoGoS#$#!!y*67}G#t7hJT`KcE| zbP`>zC{w)z-^iW7)W6;**=|IF{;x#g2Ou+j7z@hi9$3>B6}_wY9G26 zOH$@Y`6<9esIzfMWQxM#1OKrj6!3zY9M%LrGi>3D|DWB8!LSws%IOEZZD znit*gja0`;%QY;fd^pnhbdl~i#&+wlyZSwx!1*XRjR2ZVDLS5T;eF0pMa>`4Gv#BT zzj_*jHN9ugeCT+P0C3BRm6o<>Rz>4}+W)7gedIXQKUxin3N@@`Eg>#tY(XntS1Ju}=_ zB`@}MeVzwhhmbT#PLM0EaII@}iejm4?4g!SCWy(%iJ2K@42nwB=b2*8Ol|9Tnj1<@ z{Pcx{;S9#S^sYKj$=e_RogUF#BW`(ITu~_kE2Q*fkjF@5$%-z{T+~)BZf&Q^70d5F zMC33*X`Hz0bw$WxV)u6OR+OdgJ@_~#NOuUUw%tUUa?oPXL@;Qrwgf^PqS_Exf?!mr!*?*s0U+zN&*~Xo`DqY+n8l8@L}>ARcm$)K{HE zv&t|OPWV|{cdSIFjheF=blU3D>->68X{v+#h^RaQ&@qK!4Kh;yObTlMsG=cS=@6ydGNT#r_cNP z*n>OS3KS!G-sb-?3h`!Vsa>w)kiUtygCB-x-HdHAGf6ff!$F*wO4;YxA(`K=@lAa{9&R3AH88Apg=Bt8r{dwm(#=3O==kTtSlr|kY&`w!%s3fO& zuk?1wcAMQZ&Wnb9dEt#96UamhGH9!}xtXcM|9xdw} z@t*d;qyKvq>z74)pB=K929nW{Xz{>aM|&3hgOTP*#tgNsfl$9a7yOQ=Y40l@bwl%X zUC7xx^V)`v9Y5`P5SWo>s3B)IYb4sZbuPL zw6ga3ZfnYcN{20a<+PP)V%~4qr~hTRdGXUpZyF&!(q~1e=6y}Gq`^TVC@W7bDoJjH zO23`Vz1-i8`-81Zb$(0=K-wSTKQ$9uy118SBz3KzaGgZ2va-bw(! z@VwUSDd=< zdUExSR1%(!i)pZdON?9R8WrNEQaIFc?XBn(eG$5fNW#!7kE=ml%56*WdY8E?WlvH6 zSXxt2_V-i^4m&EQZOrvn)b{Zx`ZiZh8n>RF$G?olM!KL`D2x3hl_Ox&gVXKRo$y3W z9Zsrh0At|j`p`xoJDS;Mr&@n1F_r!Jy{D!ft}b_(N`8KY@5nzm`R4K*vePBBd#J~0 zA}PZvT%)K{Fg5xKg>gZVU)!Aapr+#p_Zp3aLjW)4?H8BvbkaeCbxaUmgcKh` zWnZw`{zM!0dv~F|i3X;O$_U!ylLN=!8+OI)MIT@H3*+c)`S2K<1(TCtjvLmGRJh=l z3vP~wN^i3=Z$$+^@G!Y`q&ELhF?C+{oMl)Sfq#G|Utl%9zVQxd$)DoWOy< z2X+iBy@k*2Xf^^sMs?mRoY6`43`m;>;t>W8@I9ApqZDM+EzWL#m!a=f!SVg@Ia3Zv z6-?j2eYI8)x!ikyLgl2vruPzw2n=FxEiUeMK)&=bU~6Up%jG;iEYWh&(Em8>lvj?w z7q9X5B++LkzDeZxoC+Dg!Tm;Ar9#q)@Cy@VR%YLcMW#X|dI35hZ2-(K$~12SaX)`U zc#-1oSWiSKEXB-^Jm*L_oO2<}X(I#q>*5YX*Qt2}n>YSoyl;dziNCOSCMfa8N?qkn;d?8u+s zfz3*!i|)sv)Fs1oy^Ok|OtG;m9`n*m*3&!cMR_v21PRf#N|+>O^wU{ix>~5<^y2hB zW@M};Xne3VUmP_`!^L!duEeK_G&tRFETDz$~+C*>;pMqoOk}b z?Jc(7Pc-!OgFU=^8X0PEzaNF6H3Igi7B9vMpQX&^3Ug&U)1@jq>(mfoPPUvP?_P}{ z3wt;k%ymR{?P?X42sigzKn(Eo`xTS=nCd~%Ws`%nYY#^`y9JCm4iHw8&{cGYr?%he3EIgaTb}omeXpzM+?JA-%g2%s6{2=rd5In4nq(pa*h9nY>k za|H!;wQ6e|w5!|9m+D`jTM2<#(Ofug<#waA#wpnCh~slhMrXkp+tDvbhxdp?ZEv$c zXos#>)3RUQaz^1eaz^emF*T=Q6+`mBPa^LX-SqvKO*k{H?NSs3q!>D%un2IBiMy$N zBAFoec}WUYSxUe+UsflV|JhN_H%cx^V+sLA^m&f?sKea!Zhugn zO)_6!IBao6qmB1Q=EGa8(PY%sOY!QF@^yJndo71r4U+ zChf{@dAea#t?+mIv(j~TH$uv@(KGBvGNMk}veVGB=?lYa0n>v^`)LCssUk2TsK8<9ETbTaNwGmPgwYzj4_UpnL;SYOu|#L-8|gapB{kw-3?VZ3;sKs8X0iHw#T){~&r(h6PS z>IRqb!sOrz?@Gk=UJui`nbkhOY%6YN0pnb~ovv^Qe(Kes%fK9buj{cb>|&C$u7$Ba z`z#tE0vzVJ?snFo2|+efRf6R1+z8%XHC8p{O6Hpa8_dZCxo`@vjQw@4)QPrUo6L$% zyzdULFQbGPo%)!55oj$roqfM%c614UnJU-&rHRA4X035>snpR+bq(YT?3LVpTh?eL zwnWW8?4b?Urd!OtnxW2i;04Z76{-?5tI7p(ZfVb@xO&F-IFmZ7HTPzYjhctn?eY=) z_**{Np<~q~$UV8j3r&yYomvZJXdY%uc*CRBjV7&k+7l5FXR%I4rb2|(6>`QM;+HgeIJjcUPK znL|9GV>eb;PVy|5l8|)G+1FU4lo)Y4A#H3AlWt-&c|Oe|uwFd-K}L^)_!K%OOw;st zxWJ{f3wlQ@K_(fSMVP$$tt3-)Vfx#i6<>kxGT zUy2zW4K_*kCU3lNy<<-OnwixnC#W zV*MepyK^D2%pYsPW+Jf(>|)^cOUB}9U=AQSRj$2K`toVGYyNepEUP}hLUGH2`2I+L zB|4i!F0J&Bt*yhW5n(F8w_E{pfa5$$B9kP5)7m;i&6r(-+S{G|egy5MF2U&Wru0Ka z_S;RHi#FZ1I_AXduq@NBiA7*w!68oBVbDJoY1_bUn>ylwZf zHl5`s7%j$*HaNUd7GS)Rw&={uOA`U@7M`=6M2)fb4$R?#5hx4Tr<*u(sK%Haai$8HlU_?)iA*_*qxYtk(SLWbC5 zTW;*pL2J+9&?^qYvoPTF0O!Mo4Iu3b#gQ3679*l9R4S*`!bQ5u%QXgB=r$WkRkGEQ zs{HcwR~_2qT+jze^pjSaEO$FY-n>7X!(mGPyK%TIwz*Jlx_QW7NUsD^EoW zewYeYl-lGOUYQ=V2?54x_|0Hk*i+QeQ)q8XNffyTFNj5`$r#~TW9FCNi3SV&HLIz3 zI2+re`38#8WnBCuKb>2ZEYtHj9T7Eq{w<@tCW2{kix2;H3;drTqpnZy8`r0auzo=+fn7n1o*sDG*os-l3%aQQX`31zW8xLmMK zB4dSH8%3OsjDhBHXdNvRxWsuw5sNZzSJbD<8~*j8@Ik~pLw|K-l=WX1UZ-0D(`Wii$L@J-5qaN`a3tLMlW z-!3F&&R4$lD0yD_ps)0>&8dZnpUrY+1$Oc1L#e?m$B%ik9g+l_Wvuhui)-P|psiQa z5#E+tPFWo~D}PSnug(tlwtRNOR&^w(pks?x*k9c)+^*Z8u#9oLN;PEGWsy;cC#T7^ zU88KZfJv!Fe71J8y+UPsq2v$IHMYFEk`kFGk!@sis2r(I@NK@Y0)|YRfr6+9W(=_z zs%2cC@G*!zlf6(n%urmewBE?@Mk&cQ9w8TR(6S3SCPmrxH4V`we>p8Jyu^4P70WM6 zXa~+o47m8j$}GD845QJH2-0lfvub7>GVmmI!YRL7@rZ#NH{mc_>Wjp zK&j#`_+|O#PVu_l1s{NGJ!)3>|6!en&a^C3muRj#_3QkG1%bX7d-?BEE&wk| zFIiP>UIE`n`^Ewo^C7O5JJvW6{_SE4+ted3q%=Tf_!|r)E+*IViS%ItaEHf#I8yqi zjC>`6=24$;+f}87cX0 zW3jKLc;@1-74j=JF_5G^$1ex>psEkNNt5a2;P3V(wvJv6;12W@INL;ID>lEXwQ$p{ zl&#etJm+C!Y?#K3@WcN!0!khM)~wg%XF2-yP4F@GyA=Igo+C;KxO$?5mr~9Gcl~FW zWm&El%O(oIT`(;(o_d-0 zVtdr#88&Nz3<6i{DWxT{IBVzrt@#u5tXWL zij4J$J`p~XWD1kHwZwu_(?(RGbl~9w4&;xfVKqwQ1v*>pRZf2Owd`#H!vc;LE5D$^ z8gxb97A|JLzP_IBV5`Bd8sv3I!%w&Wfom8s-r3&QP1CAjk(7$-KchD%K~Wjy^-lY< z!w&?Wt;%G7Ut|cu`{%s(2Zp-(%_SebM_1_f7jN4M$I@<4L3`*V<3)Ylvw*R_2PuoZ zb^`q%0KIpNz~Z1zl$pYJarEkDp?bS!WGw+Q>%0*$d%K~zV6N|d14>}q^!IZcb=j*k zi~KEl8fICfCTEqnKb%g9hP#I{C}F-|ES^8LNkox-l`}X%InhFG`qrP%qyVxK?L5Y* zA)|V%K{gDziSlhMD=)ZtvN0&f-Gl<;H8uPhT1o|5$T6X^in8Y3(x)KYV^RYr@NeSB zG$1q+T2Zk-WT(?NCdKrZI9yy+&`6A?@L~v7+}dz_#qE96>P+wT-dygvc#v$f;8!US zkpR9`8<_LoEmaGR;PJW%TRS^jW4jke;$Lq+Ws|$J2Je^5(ia326TD&;d8Uoq%cezh z3_fF3xny5WL|(p=#;ze>%FZ1^Vyaz-W#@}U84O+Y-r{P}w6>(7cQ zP*>9CdW@05>3IT~iQV@j;F>s5^Mg7-m$2F1rNC__lXh^0^VR-S1Wkiv79(vjuN3K} zGS=m&%|fk2p;#^DWg9BtacZFQVX?jghpPF-;$WEB-`H$^xA1jac6X~?#sQQ`fAt2$ zt+Ve5)!gj}t)VU#%@be(;!7i}BWz^s+<-JUvex1S0+&8sO?yu^w25n(T{dX0h<9O+ zTui8}?z6SWVy%mm<@__l%F@8QBSJgX?z$oM`mF;~8ibEy2Os%9CqbHLMfC>qGq85H zSdMQwY_dVX?-vMq86VV(N#sT5gfy%R+(10|PB*7k5)_>U1ThI`QvoF34U>?XYvUO* zL)uVd*KQwYN-w# zgA#3xU-$6_N!*V0GyJxhq3WFCMN3@`KdlY1sI$afG-|V_QdAreSCxzy@x=l91 zwo@FZ&gl+}4t!9-%~n zQ%uhr&vK^XK~y-V^_Gn}WWmnGd9nWbkI1^B%{P}O|2D*;Q;RQ=hB27m#Lo|e$}0e` zYn92=A}^?nkcnaxRD}2yh?XQn=6=c?t>LSA&i=+d4w6EfhF|*9H7_iHI|oeQ`s#uyh9n8mD{U7k%j0<&`%3* zr)+0$t&YE?XkX*a<$O1}SC#a&gg=Pg-ZkHi^E(lL&A)E@6q#;a7S22q(F2fGu=?>W zMfu8m*k8?t?I2_ZMCGI{S7#$?gUP~{ zR@g(lnklWs8nI~7I)xPz!j!?=cLg9r{2Yz8oy8$??xXYovcteduf;i0D;P}s4_ubr zWz%Imt%x_Vb@$FAdXrO1^v$|SK%1m$p&>xGg|9?LeaCeuR20a&{Z(_bJ zGrBjB3UM;gVk@4KJeZHVbgsZ=6S%^h@XQ9Dt==|A@*Ym&umAG(a53$x(O_ZN*0DIw z@E&D05ZX-&4xLC*STVVQ&ZBB4TyNsd{APMy&+NLn?KcVb^P(Dk)J=@%T(d^Hs?;^? zTZLC~^Au6kgMP3aoje!15xNF`9xtKJuQmd?l;1G^XbFKNkX~NT`&|cKBWjeaTFGS5 z=Z~|w(w~A1O~L8XC8pEVIrM88rq5oX&t;W77X-u9P7_|egDnctWp;s=$^r>J|(bCk}QE8HMD?p2amyC(N24K>&lI0S;Y&9zr%Fk`TqUvb% z_qD6bAA~3L6UHDgZDNkFs!Nh2`;^>d_EE)PjSf_$M_1#_KzLkqJUcSa4X1KRy|%Ui zsL=7w_;+Dz$7zQiD2%P~s7diZtk&&6{K_75gX5<YGjSPk%v&}*I1!dhvztnK#qc#c9rUMBR?H>R`E?)0K>=mI>nxmRgSjL2_frB* z7P-hVvq0zZ8DX_x~e0f4=@$y(k&RTD00 zB{^sNd-t_3(G}>eDdUX-plU*|5h`Zw9VWq|lw%4me*ymXDwC?>UHM#-p|O|zXUpj% zKh%kvfm(y;+8q5V`^9&yp3feHbY_9^&i?rhl0Pzs8=ULBvX-iXAX9sd{Rd+`6KnGW zwF)Cq&+d+Xo4CQfFhuQEFVOk6@nEh79LHksh*{NXlm^}`-Dk_`P)-Q<}>c?z3LANJT9A{_X@ALs1k}8i-QVy z!r;*Q#j9;oA492(Aq5F?iaT|_)?ZS+f;{t$Jt@IzI$xSz<)YoOrCD2t`Fpui?&Rfn zOn4uSU;uPt#L4T@9btu3k6oHh+qpGws#nWgtTVB*ED(cVi%Y7KiQ{WQ9R{+c2YwQv zPFtDM%gDwYu2XZQ7L!*C(|%EMrS}w50v_SoWlu)5n;gu@`+)2WwWy;yFNTuU%%*1M zScHl{navrF(T!Ji`_W6Z8^Zy#6pJ!xn-#X@zvSc{g^Pw+f6}+%|LOv+&&KWiY}-IH zEZ}CPC#{&%Dfh$`^GPnXhq8sYbn}A;D7dg(zf7NJeX??l9F&3D705l%e+_J+t19wl ztQj^Q`PpYo0V7}ESarI+!W80e8j8P?0QluS2uy_^ZUWNIS;^jFEJ6hx= z{g$sTp2hafjJ1;ytA~Y&64lB3txG2qn2yez5k|pUdT-E`pB=NS+po{#qO8ydqtf|~ z$qab$B`v!*Xov5lW6(WGuEF32V+^I%(1^;H;_tMDC$oYDzl^pX`gk9*nUVF%=RKjO z6b~jG9@W74`coAK2DY+3-wOC^EiMG+&A4WgZ<_=ed8BN-Klzrr^E6Lr^!`txCE<+@ zrHk;CHFcMBCZ%jpkA=15Uv5>+Q!%^jQ1?HLFgF)hZX`X`F5;Gkd>mK|o&?<@KjM1Y z>SNrEn|(b#l;>Jn6iCCOJFcbNu=tucT58s7FX3Sc&)r@aF-%j9ZeO;CwnoKEeN8du zy6gvVaZ4y~d@ZZLd@CX@wB}Rq`W=~}*fL-2XxIE#tY3CWE@-cDgK=$Sw6+07lcOPD z;^mW(9CgGcKo+;-EUSJ&_qP!H5G?mA1QdpunMs-epH1)YOySWypA@U8NVtt}2CLkM{+WEoNU(L;GI-5_+i#VL& ziDoV|SF?%hN#K2v3 z%#7LC*Si>q%gD&3$ju~!ZaaUw71Bi;Ge*3r7H`+#$t8id8%mbq=SRqK_`Kp~FScoy z5|Q8+aX#Pw4)<|$o|Jo6Dre5GM<-z+EF=(e zKM)&v=>iHmyCxXRhp%&gSl7xJ`f4F%o5tpzuHgZ+s<(zPf`oht_bFcHVo%Mq$=Boj zj^c712bQq6vn}e%321riYmFd%DSAlo4CW4_*oZN zZ8^#L7Y&RPF#V+}3A?GM`gCMs8#E%su6_IZBh~`5KtYD4THAG@`UuwsXqM5GqhCPE zij5{REu=3OV!>`AE7sb=$d*T@I3`6{NYlimd@V_NA_jrCi=3TE^ARH7gxQ_+l<>g? zKfxe6eQTDU>-f42Lt=IG>|#@L8J|1*LQS-IxRf^4e_tx4({Ek6Oc$|@r~#DjrU80R zQ}o(<0HS3A9=H{0mDd!*e0;r!gr;J&c!d(o)&2cu9^;!n$9fU6qC(-pwvj`0#+V?N zI)QqaRnXX`mP*}>@-vNE{AUFs=;^spKBq;_LGnxA9L82x~nk5!GBntrW5{WX`%7p@=>*ks#A9I*$VQFG?c4+x;y(z$~N(dJQ;ehkRiB%NhX zQbR0M@MiVI=Qv43p}*D(GVMZFC(LX=Jl1H++?Te4lDsk{TfTOi=jub|U}^pB3Im{P z-|ywonzm_qlX<*3!i&FV9{$5}!4m|Hanhisj&=0)XLWs_(myfP(+q&qirA z{(b8qT35G6acuKem z`6~bs&ja79)RsSw^`pcNu6EoW#hafWS(eD)b>+MdUydHHAtS^FEjT-oBMVF#7teC` z$%T2iWnflfe6h7vJzpc=yIU85f)fw)ML1;pQGj{%ol8}Yo^#OlMWL0wlvVihaY|rC zR7X50Xdm^&WW!o~CbWc6=owWa7-8J|q%HJuPD=d|EaqBr0Bv%%9_yv%c49M-1wx4S z%t3rE1y~RpRH+#MGmzZAylT|Ye5q4>%qZoBHfJX-81zryePSMQZF$}FM4_I=*@1^E8tReM1-XCQ$9Oo^|dm$0Ewgi9b%#}!CG!cr(_ym z^$RlA8p^tfA5*&DmzYYh#`4RH`Pf9q zW2l0ZPIPOS^I()wn$2O@Hh)UkDBjXoL8qa@XPGr@NzcX@8TeXL*B^VuAh0~E&x!xZ1L*Ie> zJE>lqdZ7!Oh_Ku>sfi%ZO#YS0$FkH6sp;>cOlHzf-E7xuU0*3xjz-9Q)XjI+PX=%^ zTCQnHQ!CVky@FO7?Na73z$!7<(wmmWgf%s&L^;857i(%>Pix--I$qBcfD|bt%)Kd0 z=@p%$+i0Hqcu%nFC@=tJ!5;Oqk2~e#Ag$8=K%l?~2b~M|rXQD5rZ9zsX2ID(0m1#p zBg6W;`Y5RI_%(c%GfiRPX4cF_KGyJEi6ugOL(1J&2Dd4z*iOTV)rCxJK@9DKPSw4Bw2(ahqIWOFqzeFIz$S zqH#pko16iEa$pSvEnWPDIpUH$`xy{6axX?W+BGs&r;`bEQcA!6!au5)FLCai#vCBo zw^V!}IC5;(GD+)~P2QPVAIy&QGnysvrdp<1+V#heQmh`~7gBo!R6RHHk8DiU2>-?k zN?biBr8bojbd7u<(YP$o=~waP;#sRsZu?!DX;tu%?+KfEr*TEeXAQ2rT!x+uba-)& z3ngQf)=*c9dUwibn6DbQ`+`40xmdI-?)AL?^-qVi>eLii2?$KBf22?E09d4mvY%`; z+hGc1DnVvWDY7<)6cRpkAUv*KL;a0ch%(FwIMJ?Yda01SKk={h}0 zh3#{mn*e0?)QyCn87311R#D|wprez^Nlr8&9ogiC_=YgU{bgKHFxvA&!@K0PL7Lk& z{J?#qu7rHoffh~+bCj+Z>iYJgh^U8L5CO_EbEZR&<+y1DnoaKx0Xrs)E5B6!-hx+~ zrH7Y^)q1&}`Xk=ZO_{SLn7&&g!&IjPO62*?oHXF+uQ?d87!suRT!#Fsr_Fbu|H7{v z%DNZd{~WMb7NGDQgNleEznEDn<9javm*5Vi^7M0gRM<6e3nQ&BJ-md(VOpEr7~#Pt zu4mbS+a&uX56TVE9?x~*Dj(7qIA30xm7n`{DC4{{ia^b;hy?ooXRDw}A~>)L1M$_>HjvS+D8!S@N$= zK9oEHWn_(?_JD3JC+IR?qa^@!WJ#Qpm2549+C#fFJAX($Ru)9dd^C66SS@(c%fj7%Y@C)B*I zd}gZr=pW)K3Wo0{vvrb`Nj_+XmyrMST*xg;Jz;`aG|aSt|YF{CnS>fmEM({X;%W0T6t#awxJTEA|sUM5FYdv#saf1_9c{Y2YOv`Q6x!e2_c61csWvpevX7il|kaG>B3;T%pOo&z&#{B_|imH-;CsPO*n= zy5cqxXncz+rLA7^COcv}>j}CNc`csDds^enIHQA2aI5p~rOEA0Gkw+~vp5nnO6O8% zammkdoX6idVoiP4tv%k{%loSskbBo$>{|SO^W4Mj{G_kGWGOrdegOdM!rra$S34@8 ziJ24>Xo6ZekmF5W0z`tu;+2c7z^`P!Ww!RJl(c-9$WHXKG{TV}90;~M1b(7AH+T#) z#`w1j7fAy14ZQdAaA-g4Wm0PY5cp!I&pKaoT~mGhqfIX{*JtsC2Ukwo{3OfN^Z}(% zm7v=s%;nsV&d!X51@70crEIwwoZ8G$KQAi62?|;IXn+g7|@XfTPqsX zc76){bxa@*Ry$6(ne7$t?>d-}3v}xwSn2;LPHTEQY>Z22sUECXh zN`{OQzPO{8!|8nc**dJlX~&Y0VtWgAox~FYA|R5=|PxbbW>RhL}Lxx@+(4|mo2 z=l8FX3Ha&o$-@u`^0oFhpnFgB%7$OlGg@olPn>|ijI9}1qcv#v=+{!%ju=1J_|anL zs>OINiwOI6?%I}Z=}XwqZ}D~sgn$*R?=N(ff-@*{{NX9Do6+~PpJGMpyWxm2dag+R z4elF{$E6-wPY-WX=gtvgh0TZT&qn*c32eXqGhJ$H~TT_n>m6$QZxUuN3)sT;R>MHDj z=8HeFe{#}?1;oaagulf;7hSBY1<}J;oJp93v#HAoh*vwy_B}KEty`>J`5*5qzS5Lz zz|OO;J)h~uFPz66|8dAm4OCKvmvTx`PXdx%(CRfnXIC)yrwNri@taTI@mEcLvG{Av zE1mqw{C!v#oVl^>dBr5~eQWJkSk<4Hf1|&PBc)2TDrPqQwOEorQ|m>SNu*ttIF0QF zcT@bzym|dFbM=<-O?m%V*T=R8mK$#ssEZrMr|cy+se*9REE#!N5F{-6?Zu~y$Oop; zxsj@`F|)6k^er7y09OTqx(^VcL9mR)+1IYShI_%cDFKTfy~?+LeeSCtI+T9=XmNZ! zc@8_OT$blQJdC*e;|NRQdYV@P9IqDKD1y8aBy{5Mc`22`5;Xmaz&S4&=* zEerfNd#sGU!SDaKf5g0a-SQ^-W5>UmbCuvf9!RARn^M0?`=TC_j~HhGJAYli6fKI# ziZq7YF?wA@eqGf09h@87aD5;<;)(`w(e0q6%P8G2pXQY!@T%XJPvjDsJ)Y8IE2<(M zG3k$tn!Dfsy#6HD|NHa%KP<|#u{Dtnum7;XJZt}9y}W)`{K@4n@uJ*bqEW!VzF%Mu zDi-VCZxsrT?&+d`^vK=E1|3cA&9@oKM;ld&h}}EgTb-$_C4Ywo{|#{OKRjkZa3vFK zeZS6#`kUM(hQ}pn=@h`7ok_Dt`}5P!?d}Jqe-@(C=gFNn4z;zOkgjd={bBj|`sutg59(LF0rxWIOJ|0X zR)IfIj%NQdzycH8P1D&;^-&<#=HoaE{GsmgB`trK?rlHR{80Io-2a=`ed@qHBG>v} z2j|hzMXts=;+eAekMHzfa_2vVHJ5)1c;siVKeejp6PxQl^3cvp&d45myzBwNJ-s6$S_&{YJ3xwS^z24d462KZJnJqhr+vPNDJTOE6VMPJXf8=QRbIyizM| z1&v7|I*!~EL0GQcUK=;CUWA*smV7yPW}SwSBbolIwy!isM4` zuN`+G8M?q*7wB$|keC%{`10wW^UK(cisu>8XO z+km(@<2zGn+)o#QhyYC4@6|hO#5?_Ha>i#VOok@a*|^teN|c+`|GQ(Te!!Rj{2oR zmnXdvEwWiyP}L%jusRvop)KTBQp`&))>H2NzIBHx$;};2Hi?fbzA(Js`tK6d4NGY` zAGggI;Nyu^f+A}igqqvRs@vNI&lO}3l4N;vbSh@{k~=D|Tp-yCA{i%w##R_%g~>yY zk@U%QI*gz6Y;`x~E9&X*l4d%cKChfComh(qX3WU1E-{(1&8@A!C(5$isx!X>#ZjkQ zSa}mt(;{twK=T6efwom9CX_(ujJ;-R=rjRTvaUe^*PJq(V_i&~eyzN@RiWDIs%BJu z`y>%N7#Sib02b|LsNH+bmiU`FE+x^xPA`+m8I@q^QsD&}gzd+rTaK1P$4jBlVcjI4 zxw4Xz`rROqxVkJVdu=+{t6gyo*R+R`Uf(?9jhTcpO|KO;ZZ3B$e9ofV?Tm??0|=sFkYg6D*$_q2F;Y-HywFBBLp~d2;PMdaiu&GNlwPX{FZ9T?xqCWA3S(nr;Lw{Z6{lIYgnl&*0jk= z7F=0l7q<5C?T-vG_&4=rjSZ8<9hTnaVuKxKIKic+Gd{*=QXa0dn76Y|jtDWT8t)+bdnGusn%27$z1J&;>mF6q$2A5Y=VDe!DimzM zp2gVk6`5IO7k^E>l0j!I7vtC#1|&ixKNVIniLqIzXBl~}{ff95r^?_nFCIfRQNAuN z<=B_9AiyvBMoiZmcGo>jWmF8F+xdxD&W86M87p+`e-W zqb8S`pp#BW?;wk1qb&+-8AiAW7iZ2wBO&9E@3dd(TFkyBWvo&LG^+b9`KRI&?abNa zjF}cB&`GxGM#MVV3iZk8cLL;L!}mzLmZq(jZPz*w~2+1C8 zMARv6r{>NS^^zvZ^6A^i7^+6KbCIQKCx|b}o3dU|pGAE7Vr3dA7rpBd&nH&6>(%&O#ml|8Wh8X-*T!P2MAj&b z!-$mPaqcgyr2teqS zsxN7lMYVyFnV{`fG%8RJ;k?-xOZ2lT#g5aN*6Uq3YSrTbwmmhvFxT!__Gj;%GVTFWsv#qCzw(@=8Q$FT z%Oz@l8&-yb5$u-p)=ZR>N(N~b>9&P1OC6pWkM*1r)v zTR+s8)q{zq`g`s*OI@8Zt{0HGa$rT3-fG&3lv%{8U@HJB8oSqR6koT^Q-dyJ-9qgL zn3OFN?N}yFc*NV4sZ}G;!o((VvQA8}mf%;Mx-wniB}8us%ZaCun3tXAz0EE~9r&n< zR^m?p~#zIdrO9Lu#24i$EQ5fI~xXNK!xR2ymI4=TP67J5W2>D>bo&C zDY+_*^_UrPj!q+Nh=4?wAJ(rW`E!~|NY2FNubCO}9n|;+(}|GXoWl*)~zR>Y)R% z+*QlxMd7l{g<*LYats}aDCqeMB?UxKm@|>_lRMriRWzp}bmXdikARQQ)T21bjCIR0@8#*~m9u<$P6dWxi02FV-;uVPUDGc;Jd61nTmHcJLjc4LN061jdB7Fyt8?GmnjnjgB<#arY9OSx(-x z^}6r0iwWkeoU{dOAQ=7sicBx)F){3q4&eyFjuA)84FCTMJ2#6Jth>At1(~WEbpO6-|WnMr7QKL^r+u$7e%E^zbIVS`d zx@kwpMBS}LxThAxUpTjJGrLV>$CDrO@k}`)w=2X%P2FZ@u7G~!I)UW9Kxi6FsT{TW z5sPvp7)mN?3UJ)LSO$trRwD+;YOXwipSG3hV2V5+bx%2Aod zj5~bY3##xXv+eP~jitU5e$eY4C6zUiEWtdDWoU7i>ElLyzBk&jFi~faQ#_3fV}-j- zal{?woBA^9P{_Y#Re1)olvvi3of;{~6a?9cs|8kwDpWq-C072PEQt+c#?D+S?Z%98 znKKr~ZE4AJfkB`NqPk$cV>v#Kd5Y5}fZ|jo6$W(>4b;!ln_5u<@u|w|kp)sbH<2oZ zuv;B@V@n0&)k@72WyvmV0Nsbuy}QCP)qb{meyTsXrjuZDU##U$SC#q9dWzh$cGaL`5 zbriDlF8=_PYeRW7g-L{ymR%=Kf$k(h-6I1JTw}3Bu96~kGL}wJsYRG~jsvoS<56Ro zmS#$`v)NR+qf2!{3_IS`Wu7g@21?)6S%>v5$f1H6ER&C-jPNH))5H3ZwW7j&LDM#t zK~tma33+1DV=!x*`P@j=Oj)udnYLF+IyH%_eBP%x>Z)gS2I@_OnY0w@ZK}$Q$Q_iW zALL=x4QXT;a*ST%{obRI)SDgte~DrZ23;7$u4nOFqb%f`;lXnV3WE(&C=Azdsot() zx2co4^mVzGtPG0xq}0y;0Js)1iAO;xtEhIV{{YN@uS9GsBP%J_8Ce!MFQ;m-FsyT{ zgp@3EGKToiIasPx85Oz;B$u0v&rLydpF@{6DSsAWuuG%sqjBOAmw8BG1EDY zXtrMdJ-OC7{f}-++MRIZH={8|G2KFvQ4*Oe{W)%Hc&0hunE8vv@s%E&hLA^_*v@(f zSXBoVr`E|vX?4>ZZb}AFX_b+VfQVF$=G+9kI8h?q)>I4bV1XqTrNTcKl~ntD^@0&X zGk==zkbDy)Uy+!(b=FThuBF#h8*22lET9fRG~{uWtln;Bvg1%Z*Kmr(*pVy}*x0|@ zXZH94KI7V9%Pw|h#g!^dZ*e-A)TB;~DOxAS58L~1Z_+WF4i9usY;xH$asDPEWcHr# z@7>PowofF6X!CgdZ*P+-QX#g$Ya1+33pt|vla~qQx*fIqq90EtOu{Fv?aF~j^K8Q; z&&Lg#!R+3uA5PEY9pj#w$e~hZchjGb8^Kbd5B7S2-n_ddtB};0q1B0X)HePIU8a~> z&;>2EXZ^pjkK~7S3p0fbBe6;xML@EOIkf4Kpf@`Uw!135toY6x!E3Z(eeDx}h>}vG za}38XDTrFE=){DIu54Y3PiQG3l4=DBNXb@BiUyl88cee*1^)nTewxhT8jLZJW&t|Z z-Qn4J1q8;)jtbsNO{dcpx{;1mV$-n>a(C0VCQDEyT8#)noLe*m*L9OZ&Uat-P~c0Klns5MMo(V+zxs$QyOWR8^d;pDQaVX36< zv*pN4SxuB{b$0QmDuz;7BZ(`?9+OdY0V*V@jXaPLv*W3OED$b6t)C3%3o;5!`-l{S zCW^0=MP{5+(V0ibl@0XdTl!;iqehhK4Wz=JzWN(P^F~RDV235 zfN{V@W>sMGBW1=_TPJ@pP{D}Bh(zZ0=^DkJyrNd-1DW`Jl-UVLh?GK>bCNW&bH;L3 z=YJQp*?r{0a8ODl*MZ{b(`v!^z%hAjVlws8U1mz5CaRJutVk@OMYai*LG)|-YZAnk zG^V*>GLfd1#e!>ZS{kmJ?Fw3KMxh+a7PlKpob>$lF3WJ^u%1Fp?@FNbP3b^xft}7G zw3l{WWVLY0tMHW7kBe_VCKwKf0!|SnTQWO07UxbU9LDJ}xiK1*U%gD&1VoC?mQ8L| zZ?jiH6w+3LTvK91cH_8BF;OyRT}Uz6`56UOl@e3gR5HzIi94DDAhoPp*TL2BytZ$tWgW6)1ZtoRj1iRT&@$!6@Tbl~#4h zDr8?9@6?QdSG71slDyHH$Bm(qM>+g^l3$PI54HOUi13YXw>h+Cnk?i)E@v0Qyg3V? z4@TdtG0+&{nKWt47Wm7Ul;nv`Pq|q3rUudD_n7fbqNs1K&|yuW`Z~^Ic$96-TUze^ zDXLm^Fk&B?Tzhgg>2P7xjdU*9I#GrwY_elHD8W?>HWb@d?AVOGPDg}fad^hLSsNUL znK%Cc;$dsQa-u>}l&T8Eu;nQjUG?Fum>ZB>Yf_>TrxU0$Z52=qh1Ef;Ho*dKNo?&K zXwNvQQW~n{VC=p)D!v2N$pFmE;jl`0rFpL^a{Jj1N^2q~QmZYmAkF5Z7{{wkd-1sA zW}zsEI)w{Qdfd-@A|*pG#1x7H;>q}g!nEt+6-ycrNkj)$)5!fU6PYx}IVWli6XR}r zLvwc}q(~1VD}(7pk_k~65K)?5__NI!`Q6fLO$p{_TbndNm|~h9g=Jc+HT5~n=F2ZJ zO_@8SYuFEvP$a)TK5fSs=1j&)z%c8rYo4|y2Jp%-dzz~z`a)ez=orYcsZYu^wY#UT zaSOk3ACKR^Wl`-wJ0_^rKn)U<4Dw~o{8^JF1uOhJAZHkML7kI}D}b^4g)u5!1!8-A zPY<@l8cEzz?rvffIE6BnPpMtT!29@A>++?%L!B@9L@aUX+;mzs0+lnI)kCb2rn@_< zelj)~VgmpNnOv4+x;2GA-YBIZGh076CF+}qg)wI#okT+7ewvtotp+Ai-sWd(_S$XR zDvnP|Vlnv5EZLH~5TJPR@ka$1Y>7{ti$2SiK<@KU5k|x+)5#xC3~#7ovD{A@TUXR* z5SEeE^=~^fVPnUasEK6HWTqDzPixF^-zh3)Os-R??v`HbRca*)wjB@`Srp_bwWUP@ zfR&>teSp{^h4{q2twF>V4$fA!kxgOfn!3zaR@C_kQ^dt>rj?EgFrywr6AQ4kB2<&84|K-4%gmdjZrnBBxO%qfZ&g*)LW^?(aY2CA7!mU z=U$y;=^qo^M1|C^p*vHKF>slU{^j23iR6dv$MbcTV`ovuCr}h$hU6=gL=*suG|E0` zlVyU9n?Lf*+1Z<2zLxW4f}TArIiiH=-AN{>#pOLkR5^S>tPQT!l_W8q@r_N2Q8m;l zf0t>Ptnpk=;3J8V9|~Jy>Spz;>RqBIR&_l#sVsZiqkL*LUy|kW)sq;>)6A`q+9y%2 zU9qbus1fj)9}8ruE0Z$HmcE>qT13K*{kZoBhZwe^%6>IE#{&0NRP3;MyJL2vpq$id z4ZAGrtGewTKK}rX1yG_HOu4rwH4)k?hGUUPq-{at z9Q$5k83BEv8&Yi%spD@($;UdY6JAE!(blYuQ8Xfo-9u^NBSu_|mR+;9$W(pH3mkM~ z+vAvjU_myMWHpipd5?=i-nCRF49;aPyTy%GPR*+i`xiSfyA>QsU8;|NKq`J`MIuH| zRLZ2GP}HQ5fxBu+D@DVr9gcs#{{TlMuvRRH=e2eoX2Yh+XYXk|(X~ojteK9`qHmQ25aCoV21vofGw(j0 zJf7V9eL<5}X*+YH-S6gOkA4K+W;YR0o_d&0p(Qz{x;9tTZ;a}0qR)D~9@Rx_Xc0${ z=~=0vW{~S0s*IAYhaQ+x%m#%3!n&{IO}9TpVLY4n85Qt;Zd3xR5%oBp**OfCO$2v& zzkL}jzpeE;V0WD{3u-kY55&Z2eqZ%}W9nDT7r{@>MV?QXFE}nlH{l{wN(+;;2zJGDT|qv-Aq~`E--sc%y|qlHF9}wVtcFa@-ppd{gY^V$J>{q zW#qKfQ4)-$U*&+D=$fL*X!AH{ELDn`BDBVU9OjH;Fej+>kSXU1UZzakl9c&!l-o+* zAJSfu)(w%%7Cgmo%1|i1WmJ<>5gp-L#m?qTIfc=}b-9_7N-lMfozR4m5|ggu63Qa# zL1#37_VO1vT*p%;UOBipZ=U;*=SJy7H0m^lqOmuN5Q)hq*4?yWY|HQES%|fJvXbrn zw_UMni#qDGmNu=+qL9vk`8`uA;JBXiRyu*Kz`@)n&^VdPeE8CjaXm9n4a@WR@ly%f zME?N2ep{6J#*)Vv)MSX%58L+A@LGov*;-)kD>oA{ot5c98L9=HD)knYfTl!Nk|d~a zkf0V~LsF`DMk>bGjCRMoa_u;vapfG{CK4jT@^^}rG2)V5myadjOr?;TO!iWY^5CON zcd4|fOTf=$W6{jW>Q+&O<)hKg!m8EDn~6YxC`9MtH92i`Uecj?vngD&ESS-?Ow55r zI_}NP3wGno@>aV%p_(~$UflhakD^=X4y-vpP(B|lCqX~?f2NSYR4Bpa=jR=yW< zlSOi?an{90SQ|^hyil}nHZ$&!$JAK)Zb@3ge5{B9Zzu6d#62js?IuPm$Q?tO5jmQzsGn`-DH(?#F4RMD9m<&o$e2z6B-8tb@DOqo5v{$&!Q zNkLXB2npUQxh_zSkWq6RC4yM;7>qM3f&8T}lg4Dlj9E9k-X+XOWi#5u0;<5ZZ7Pi# zf{1%5qK%XSNnu+|Y<4Rcf#)K{v_khV7Hd^ZCC3|q1pV3bU0*qvf|FCe&_Sr)Iles^ z$2Nni)=zkgQ@!9LCh=zE>!^D*qG?L<6;gC_$90=2)I?8I$%+S(iS?FNKg{-SwR4<# zvH-ex{7h8~aqXiMGk$c)CbMq$CbtrZ8R+9Ds?IJ-YhFC3;wPva{O(Tg_-;q6?rdDH zbtv^zpA}LxJXa28+=c)m=`>#!Rd7`o#j(4wW%jX#a%DZ2B`ck(exdwqcA-Lx;)pU0 zkCU-4>CJbIK$+tc1(mMF?ZiG?LRR9$#7I#us2zIOu2g_Jx8EO`{|kip|Bw<4LKz?hBNuO7S$Npv+B6M;s0#ySa8yqy=2SXHQp7z zVJy5;ryFDo3>-Le#yQ%lH{WWwajUCtM%Q*` z9*kPy6O%7P39nBn1=eL(7kMQk#LSpc2B9fYSqg$g+Le4tb$<42Dy;yTgt{Os))6Wc zHWxbo0Oz+Zz+y%`aUCM0ta7bQJ4>acy7ghG@=eOr*i-P<%EF42NgQqG-sLU-0KAKR z(L1$d)*>to*3E5c$7LLcRca9IT0z1ZDpfo}B7&z1XB>tHM=Mssu;c1Vika0!3$GuA z!q>4N?91u318^0OEpc@yjI+;)gWY%I7MYsz+1}L5Y9%sc`5kp*)D_T|dn<8@1VZ8= z3FS#lwsb?}p9+zG5s@RQG9&qkSx)f6F%EpD$Czux$~*IX{&vw;V;(-5>KySk5p~MZ zZ^Q8K@*;INGum{ny3dD!U&j=T>M8`=ko9t=bQD}iK$BfYy}JJZ<<(fR<;SXLKT{`T zl=Fha>|?a9B}|hdf*M+jU(@X$%ovcfUo%>EuvAk-of^>>lF>xz zIlCg+x>w{bjjp42ATUNOc7v+!wpN1RzE_1?p(ph;CwId(bfB)|r6DsTx*wMr7=px+qdnjcs#Bw}mI+mo)Tx7wMGGemTXU0m>iG{SUHKVTCjtN1Oa%n}X z8qRWP+2<1;LLr(VlQkGMnM;>_dkboF;=!G$T?{>JYO=EGvs$l})VJXb6-TPUV3OhP zG82*lB&glEW9=elCS^|;)JMPtSM5I{&D=Q*XdR_aNn=XEcyxHpRI;s|fgLhml5;Y- z{{YCrQLH$N$%cO!llcJkRn#Ap&Slf8iFR#t$PeR1HLhD!s)r|LAftRJR$TEJaz;Pq@Y$6;qHj%<1m%Fd z_+_nVxYErjCWbWPH(Z5B#;yMVHAp9q4@RMOq* z$f-4LnG^GLNpGt|g(Wv1MGUf>@PD(@xAGUkxYoFYSu56K#X^vDt zU((|mDEJMmu`Hc)O!dXu6wP9XwcRGneMDEV~KgMO|pGB??AYK@^SSR}$q= zPAM}ooRt=N*4$Tb`$VEIY3?*$x73m~!=*QWqAR2pNwUnZ3yRVs{p|6qD=m11cwy*p zmrqMDIdUjgolB9UCrQVD#F;e+z_R=nnuiUz!Oaa6gHb!LWL@3dn1)kal9O4WF2yF? zfx7^>bmhm5$upW&U1t&1U3}JvkzQ3~l4h!H66K{VCniXY6=oth3FY>MsDWRaUJi>Q z?C6LYx|t=$Y)tjc8e5am)XI%zn$1kxuawLg20lwIf3hO8O_@>D+0K~~RrUdj=ibPZMP?e()%nFre!aIE+xCUY} zPBv}IWW-u#FS!N87}hB6T&Kl3p}h>4&UVU}nh)4#c6@I!Fe9{|8>?3A1w~j9hBFB4 zikZ}wL@2zUhjT^j{>oplDs8Wl1+USGk)LiSAGR?H(8u*Iwb$#ryb>3D@|v=BY!2zNsEIYa+sH#x{P@iKFZ5be5NGFWO)-Sq^8vVG4#4tkrd3$=2l7CtA_`Kpd-_9FZ#_F=Bx|G+fVb%05uCQ7KM6 zM0pP7-Gv_5Y8I6ULZHQ3PN+v+9Te&TNUN26G{YhB*&TD0ImxVjD96*LuM-4MRe4*Q zCyS4_)QFgi0X+llauBzhTNAn|GXqdH%k6P&eVr$ibA;~IYSOb>cNIg5kHsMETL38N z$`yWHhKPTU{TWp-1meJQMlO$eF=>XUT`qltEJ=;_$spM(L6qf&3BaQy-HGJ|8J+l0 zYbNL8gR(l@Mbw!G!Xd0fzimHm%VS34ta$8s0y$;`umy4CNh^eZkA@j)BSm2z*j&hn zhVN44wGbkt+OZ^>>a{lYp-H$eSkZ{t#;C0EX(;0ZvFBt*A2aZM79O2zZbLw~!cDW> zc@?N)!gZK3U^eq6qIk;a%789jIG=IQ!~DTAAivwe_i`Mz6HkZ+`$JPy9TazyHY_lX zre}O&utiGO1<27y6+MmB*QFKfbZ`dLjQ;>^iy(EA@&5p%tGmXxh8nFseLbuqCrpXD#(p zKCc&6Gkb{1lecZjn?=6cf1M@9p%cHJ+E8(gbax8{VP}t7Y4qz$&Xg2s>=*@Qn9*DD z>xG`{^y3OnJ@HlJGkYDwi$r}RvfV|b(=4L-l~=)8rZJN_6NqV;*Kt1)6Z8K7S|=ep zw$gH6t+xr93l+~Hr72D4x*@eo1fSk4(zjgV0b@$!zD zJJ&N2a%zbsoY-U5wC8(J&89T1j(-=K6-{1Fy=*3| z!+mQRxsCeL*QT&xQ)O^UDz{dF9hn^R;iep!U>w-gv&zz8lQoqQDRbkY2fo*mJ(z=* zm7#altHIMo>*YQ(2oo7TKB_6AQ?t{P>t$%9OCAjpSpVv_^|*37eEvXXlAZ}nRU z!R^B-bo(V2Y8KaJV>MZScV%KCE0l1ZfhP`3(=?qE9G*8Z5#AimmzwdKNDy!NX@rz^px0Aa>1uGT}=0w z=P*>I#sx*A+s7mnM7bVDD#vxyu*FZ9mpwJ{YStK`r~`Sk6 zJkwItEQ`1qRPO$Yf2CxP>Ej9xZ*OSa6=(wVj@Y>UM+X*%QBrEnr20mSg|!%R5ev(I5m2CU}Hg_(;2>=0qfPR!FD zS&Ufb2|!+xA|O`xF~%g81W>9L!B%fgf|Rq8YK7yB{{YKfMPhO9e{aS)#-y7Pq;|3? zoSo=M3s}xk|yxU+W?FD}Zl zWr&=|9u{%NzWwKokI91(CDm?XDG2S%>;rODfD1$ggIao5j!cseLLO@9wR1$aszD=1 z%Bb0%;3FqdIWm3t>nGYGg6oG(6);s7mV^0cPcD=N84-~hmy$|f@tkQM$aa-G<05u6 zR#oP-YaIhx>JgEZ>T4I>+)Sa^+pUAIiQPZ8lU2M)=ghIj2-n>Y6D0*Y-)Z(^%y6Q@$m3ITVUoE}p_nq#Oy@P(`ZcRVYSW<4AGOrV@?qu^|{cAmUS(2r` zK=BNBEXV0T$>wb=yl|~+WmK}|4J?R3+yye8@>*8p?g?ywBdrMRq8-itHi z49y}HV^%fGBT7|gV;il6s*<8BP}I>~YQY;))ljTv)Idw1^V(;9@~|>Ix{G8=$5XQu z94{E4sb;lvB3iW7RCe@YvzrQu=;WWuEgZ?cJNBMdlk-at`-`Y$w9}LbY_KmxO3hWP zRiTBOES74!I;I+n(iEfW#51cCi7#)W_^n@*qd3O$@UE%=l5+H=EAXsYsp6Y2YK}#X z7hNE$5gm~xuSo94SD9LqI@_!cv{{Hb$8coqMp@2<_`Gn|M%Yn~q$y2H0C-) zX^aXjRbMh|QEJJ@3on)|HBvG^va=jBVCT*Jzq`c@o!J(4KvBw^lBbWza}|!_mW?yWCchAHqRmLF9^B*Tm&RPeI@S0%O!2Ow4(rS2 znlk!{9;W0|s0MyRQIZm$YjPRwNBlGoHr0;$iG|zFj-`{U%5h?nWmy=GU0YZ# zWS$66Ry@&%^h1@cx+WcfP7q%HA8qbe0B~`xKq)oTG9{&J{uO3UOhRcGi@47#kiY~XA zM$vTae6lQP1%ZYpmQ;S9Y43Z8Te(-ITJ?&eUex z{{TH|xMGdytUc=<-w8nuDcc?@JD3BM>$P5@48x7;Bhgpe;zK?>GdUFKiB(u4o;OlhfV)uZl2a@WQ3DhxyqAtA7U_Fj zhzLC$=%}}lHcAdmTw4pxj_OnLp}eiZyy`>LM6ZbNCMjPfCOs=E+@kR$%>yMk``#fE zN;xXX^F=DmWZInzIg7Jy%!BB&oRJZiJ|kORR~V`(!Lpc1T3q*ya#gAGMHn<99!N8+ zeIv0yKaF|(=CHdwl0BO9Vwpp`RfK zM_h`UyUw!j?^iVwM`}1x)a>V&!G~DydUq=GIn+sRDa_jo!77 z);cmdM`dMEM5a{~tD$pL28IBYXQbCgLZRsDbzH2ph=`QqC7!kN$T7fp-)E(}RZJ~J z%<@AfS3<`SkmAfP+oamsb;s7+xuS`fMKdBTb1<1_bG;g@CwNY7wdLtH4J2aD4;v<+ zanu#@W;})dyD(ljxTvm9q+{aOL1bQb;qrCMkbEMXcgA1_v5a6Iqr)?Ay0+jeJCb4CS4A{gR*zLY0rnvtn8AVGI9%s{w-^?YoRDawT55s^+! z>SodQh~I%WSO3iCw*+;2mni>;6s=7%SJY*yfInI$c$*1E2}-Db0#KxK%xV`Xx_pmE)WS~e3>&& zL>sPOJ(NV6>J=(MFlFStP8gCqB&h!Y*_x&@6_PV#d&Xg72Jk{e?1E|#Lt#kVMwR7w z%JbzQl2t*7k0SvSD#@GHPn_>|(RK@*V?^j(`yHm2Nv4$K6%6j6mJ^UHym^YRAZ&n; zp*%9D=paB@quQv+iA@?PTJx~7+Vb74iB&Uabd=Xp_m2%y1iX44KNYk@o^WE-n28I$ z@-9Ngz_Xgoix5PdYead<1Y45^4#7cMQNreo<<)vmlcP7$#GF<47?LxP&jWtz5t0f= zRjk8%E8!(s^Km~BhaGX4eYwjVUmq~dDx>h_kjEZV3@3^e+f|5iUXL`cXF5H{B#lvu zD5}u|ja+|5I=Y1z>nzXeJd^GrRu!uzwgggjZB(3JRu;AFtZr|*v{w&Bk$p-u6_W2xPi+8H7OBK##PCRtd0nux!Qlav#?&}DR z;l^MQ39p%{-S(zc@x>-%w9WaG1i(85l1nM^6XbkGWnM1IDiu4#%> zkE_?Z$^bEC9w?*Ie}t<~NUyvbMk|wJjFKk<@RcudXQ{_BU&XQh>D1nL5M+l7r|Vf> z?mF@*BrGXL2^N{A;w7a474y=b}4Qnw~FpSyJfKbUK~V~HiS&ZwkAN_MCr}!4(_2^ z2#JrXlti52vwDip{U88nt0bjnR>@}0-wifqa83^&U8qB!tC{M;XC$1zb!eD3jBrv_ zl2+8h5h5s>jTEA0HyXLfgee5dMN5c_QaD>*u(*rbF9)_YU5h+*vTX%e&TbNXm;4ib znCcm3CRbD9favY3DcbW%0G!5Zw$#ULYQ=w@GV3jZweEV*ODoiIiG#VRnY^H#G45)p zWXx7)Ul&9rR5KR5%Zq!5FYq<%tGNP)xEmhHE zuR_mRFJ`xOtl5DTV57GP{{X`O09SCHo;;h9PewWK8JGrmy2UC3CEIt{h0`$}6$>1) zhVaUZ&uV$l!zN>#+9Depu%2!UP$_9u25AkOqornI)pw&*i_qr+%G`8ZtKj8ODxJ1) z%a0V}>l~FvTe3d{hM-#a47FpI8;PV9sM*^fPFS8*2^F-XV`9P(-m%GBv4{h9?!E%7 z{D`$Wpjivu=F)jQj~tNKRHTqM%D5+B0sd1~#?%fOCCv_3N}0S#m|sll-h7BuFl@WC zE^RTKStAyFnWra4NtB|Z*42@;cZC+00!L|`Qqu)09=gr@E;l8e(8sMQIto1%i3{Rb z637gTKI(L3`UQVOnzBR-aGORC&n!Vy{&ezp{D=?c}+2NPCT2O)qAy@;A4uy3sMIEcszw5U8y zq+eo5HOgd@C#yD4rcBE+M46U_9V6|+Ige{eP84XEowq%?1p+2De(FLInT(>&&>ciA zCXUNAn7MH2FP05EIO}cjXV)}hj#%NDaQlqH`3yWbj4+`ukC|2#Eo{i2%-f1NAj4s& z(~OzAl&#cGl9-7O_1lTORra&BEwUvw(JrQuQbW;{H8W@`b^yF_X|fUc7H4slA}BZ1 zD=G$6jg?-WA8K0@t$A?pdJ5^)LX$G>HMNU@%FU0cr|_K_Fub9sTQes9GHE46CfPTM z1GCmEIws-fXw)Uel8#DpG}VwXb-F8VS>GDzO=hE0#$ja1Vv$ZY%@yY{eo|~;5+t5b zys;4{)_8DwnCINVCr7y?%qkO;=Tafr#M&t4Wz3?NqRU@eG}n;RZbYFJZ~KAMMnKy< z_mRpmV%h~|38Ki6^~br#(lPClF_=-yNf}u*QS+pc8dS!TM3!o8CrorPwTgeIjzUT~ zfo<~b*JP;M0(0gJ=kQGo)iU_{l>vUgmU5|cw-z^ zyVy2bg-z@G+Z43ni)*Pflj@jL%|-jl;V$_;Xwrfd z2WR>7XJYae%aQ%zJKfy25yGpgIsbe43`E z`|Y+=LZme|rl7G@11X}f!#gfqnQ^GhXB)?imyd~7((JN072{cCiycjstrKS^A&$Kn z#8tkPw-KY-xOu*u(KLAcUrlx_Zb}x+acPIXhE$orXD-2~S2~iBW&~@grhzqeEl5Nh zkh3pqQxbR>u!(g>jz4~^gegQnC~)WmK9_!?kg}M%v`O>$c0UCuy!;r*vO<^#XK^fb zSThMMmyyuU!cDV;V#UJKmzLFtJ(zS>S4D?j+R>R#9BUfnk5a-7v^OyR1=MPW$dv3< zJLx&eB-1C&QrV6+b)3%KHOuK)&&ptfWo64X@)=hti2dYQOnY_>rat4^H zlf^B`B*L+byGu|M-*Mx>%ALT`+UyU@n%uFS9(15urx~Pney4>qH4XyKcy0FhWjd-Z zWz;m6EEKk=l*F$fT2w?+K^01)Jvx*HIZF_# zNXpRb{3wz-k(eFEBp_1IR$EyUMFw@X0dmAlPN7E`RRpNkGjDFFoX$=zN(j3pYj&B9 z$V9-t<7d##mXol>sJdxC!vtl+63nR9%w}lx*+1p7$W*F@`ne)sJebL`eg0P9EB3cH zJcaBz*$TqhWyc2&GGtv%s#_^auszpO9L;3)B56zHq~6d`xN2KeS?X$%7O0|5;fn-$ zp>M@BqUvx?lP6KSDqpwm* zMr;v#wYCsy#NI*d)LxyG)*gu<_9kZSU8RVv_NY;NQ(9E3o}bU}=l6fO?+@2?o#%19 z-%X4`4Zlu^RjZ5d5(7Vuu5}2lnZI__EqNwp#WhW_t*$*tK9FhWBb77e2kVw)UcRz4?sCj5fMhCGO)VOhpvzo>?ZN%?IQcMy#MT(04sHe; z0XdjD`)3UZ;;kH`*@(0Sb#~T|GBE$W306wQN;DTb%Mv5ofmg$fX1L}|=@&#(BhUEl zlYhFlOY@eSn>%AE8^EwxYyd)KgG829K%+a_mv*o=R!P?Kr0z5n<`>0Uveh5K6Y%4E z7V^C9^J3#9Ikxe*?0WPICJ~|kCCUqy$uCgLfq7^Ps~q?~p;0)VZ5rfqj(F5O*Z_Y^ zu^^jkUPRO`?TI~vJx;9wRq9-9{U=NArS|fBNwq=o(KdEVialAkMwQWt02V3JRfF$6|YG%5D zB4y@|24k;S0YG2x+wrEnn;!{HG(KZs%thCLWUC7obEBC`XyQYw>YYXpdgy+1q@jVS z@;YV4x$gb}W7QL&yT*?~ccg=0zko1S^svx$5ozW*h+Hin6gkfPh+#Jd?t~z{e3Pjf zv`rd7q|6&)zPM($WxjP%oommq7#+2ytS(H zAT{^bJ0TA$rLU?&1w4Yz5GW)ya8iek*F*NP>Cka96X< z8EISoDfASzre%^3LK+EMsMc?3Y5l~cE7H6v1b$^T8Wu*1{d|tkPGrxR(DLM56C83oPZ!wF{ndOq z+?X>O$v_k&Xd+`XHGxM((fu|O_B~{o|}2hAoXByrIoP%cSm0j?A$RUht+ojjei& zhonFP+kI1f;XSbUu?zjqjP>UL!FNLRhmEW6C!4(cw!ub*fpl@^)kauS4QDwfe*zch z2|W)UZ*kw9w+X!@a6J}6oDiEZLU&ERE`6kM5?+%G~DY4!ltj^wp2oQ1QjzrK{i%WaFO`XzBLl>f=h zfg0yvxR#KKG4-uwZRU{Kd#S?k&+YVJgC;YVEs3ITdcwQ-oIhui+S=Tic8jIwqVrI% zvCqsxrO@ryYZlv$OXKV|!Jgwk9{@(+69K6P@`Klqwea;Jt0syT?~TInlK3|jS4!}| z=GDvvb^K}p|TRRNCl!r5MO&qaSOa7`?=29CZJv&l%fnu)ISwuqULGNv1CFsJ%> zCyFd$?|Z7SE1_Q?dJ%e~l;-d2;QXs8epFfF3{3p)Twcm*4wjNplF}K}dQ^z|o-H+b zB|zz_ul9B99|+&ASE6f_bC0%K?lm`8OcW`(9?_O>R>-f>=hiW)gOZIjXUTnxk=GjT zVHXptdF`unf5|RuPh?l0g`MMiFY~^MyM|qLcKKJleIgLIuXOiw_iwmnNj~95cZQXa zQeSWFTooj37&)?&_U-1EZ|nY)?f*wm^!NE6^Ui;I61~C#4e1^~_pr@(8)<#|?APn0|9`~V|KYb)b#6SM z$awZct~27zuZU-Pw*qs|UX@?IK6l+L{*T}{X8+TwY#ycdO5UxIa#-2na98jXTc$UI zkLqx@fV(boS$(%L=gfxv*Z&Bft>K;-ZU5b!YrSDMdZn1KZ};OuC+x}EUD)T0A5XS8 z_-n$~U+(qZx~~JRjpLzRN*{Dsge~LWn_vDnfj4A-()Y>-p3ijQx|^F`yvPQ<@oGap zyqNTh-4ve_pL_$e@3rs$gul?4ytG?4m-XaKw3F{(iIU1e0O*6c{v5mS=a%*D_=YU+ zoqynsn)t4rhu_v;>eQoxHDi8PGg3Gs9JDP%J9l!Qa#(LJV8uUwb`F1sh-m0 z@D=_z4k+J&MHzR!0;Mm#qabqP+yG4weyomtNjoBSj_;BT@p(5&e-lxpWRas#<}uCC z8Rc+c-dJO3ipj9tE3%p}y74afCZ;>o>$CZ%NwSjmcd+6iE9;W1RfciGAt#raMQ(BL zTnOo&==SYgGC!&~k>c?Z8D^k2h)^ph=5N%)6-eTH%+3@^Q6eY4Im7EQ!%EKIEQf`m zn?~nJoTlizUC#sSYBYbl?H zIen%mXT5ZZJry${S^v)*7Nh5rP+XDV`Iy_#_sy9gm_IZ#uJ5P~%NXAOvT_L$FTEKSwRLE99w^xeI^^(z`KFFPJ87h zFeO&bK7wuvFeNWz-8&1rJdeMyqLp8;27-7?;ig%H`9A`!@%6X6x|A=n1!VO3+=hZ? zBLz9hv;3;@oCdEo!;})7BK)Xwp(!SMQq(k-YrXHE|X=qx;{apYQJ+Mg=}zQXfi^$<@<~CIqI^dymqNo&2g@u{qli zi>|5#5TD4nK)1;>luKf?R3z%3xlgYLBPTOVfNz zD^xP%UHUZvW>)wMH5O33IK8E;!^uT*di*C3VC;7cCB3e$I(flR63z=7n_dFrN}*@y zPzcMJyYt8IER<;aumXq8YMI}MDaMiL=Of8D^kw_+6Z`AF33}NsFSqnwCA)}?$Uo{4 zhH)|M=CTFESj~|4+&>mHG6sK|PLNLSFduY=T+^9?khmH_l}ClD=8+nIFmD6$PK1a2 zF8v|jE_dyOchqZK%8gogx0~T~a{~>|13sVeD3uq4zoL0XqyZmG{%I~g`!dh`i@Kt$ zjJALc3f%jR46&>0P^XW8!>u=6Rq{HTvZ12!?X%Ep{@4z+#9#DX@7xz{)rkn+FQbgG z)+D6wg&f)5Xo{DF#IVNPP6in-3!)IP@Pk}a5<&R2{6PLRmoZc=x{8zzIE|U)DDOb% z`gLTHU0WlJ6bd6~h!QARf|x748n|96O)3qHrrC&Cm=?(}C|xmLD`8+Y(_CAl$?QvJ zfB#gW6U0g-XY#d&qE@}vu72tc3dT2eAFbkydm<=1I;5>^9ny1}tc)Y(q&8cQ_ctIK zLJek){}GT6S@fg6)yy5`FGjZ&o1XMpZj@?zWk<DNy> zBFHwnnKYha{e(i(xFPU-6G4u?$8R2K-wQy;wMO>>}OQ+Pwq%w*{1zn-ofc-RG~FIsSVL1QxP{c zxLp_7gzPOY?}*6-V6-KJnXT`fp>w=E9<2dEke2Nd(>cW=@!RI9&Ut(-G}1Y`bTv*( zQY#6+6Kj`H0QX_~6HqnQfEh&N{n1-Cd-8lL*g&hib<^mmM3(Yp_ZE|tr1;3SHgH>R zD{f}?>Ryw7wS1ooWngo2k@2j6a|53zCluu^PsA=n$f+H^rQoX=b?uK zGE8kKJs$eYZ#3o@L;)D?a=_TXv*nhI*dH<3CoF$YclT6YRj3{{I>nNf5u67Ez1g#7u#{N0B+Eh7Y;d{E zm`$qf4tFN`d9$hzqd+kN%sCznPAtv_z4k^|E#FI*9h^4(V>pIf8yDtDk}L@t=mJq| zbZ-XVxfFq|`Q9P~&ey*DBguX2pYmM;g8w|GzTU@rnnReGX$i~XDX#EZx8!>DZ2L>& z=#Lx?h@YHpjKgnFhI1RN;_GzlSj!F<1!&DD(#E{i%*Z4Ldjr=<0&zLtOXr%8IsjQ9_{&XR(! z^=HV*yDsunBW_r629#k_z?$bHz*+k@vsbi5y(?H{!$8UtOfEpdh_+jH65W=xD?Z{5 zJ4czx_rI3~)jw~I0ZkK^W~w@`Nl9(>!_TBRlf!XVYVTrGs}ck`-1{}b{6Edp&)Ltv z$tf24p8s{L{56eg{nrT}lJ(_EzMJ$y78@i#>cCq*Y`hrd zh49-k(#^*vtR~1=smhmQj$ATAdr_y046seJ?1*R3jBdM0vSo#gvA|5d*n{zBS#A#^ zUdi}venCkZI8wuxP2&u#8~JH9)U$jFGZ^hzf0faYUP!9rQ;emMZI4u5We;xxb*7M^ zNEdJS=LGAPW=vawOzPyLYN;Z_{64H6Gx%X#AIK2pGne4)M;q>x6MkSte@gZ1Izj-+lssB8hgZ{2lrC}y^a&EQtt{U z^q?G^A?()URwy+SpW)OO=AZDRHD)t<>7&|FU=oCITOj670?4I5WdJ;~ym|7_UKF}P zyE$ycvlJ6$8N2an=5qsX^Y!8f_A!@1OFFEu+J!dByL&=+t4xB3mo2v{<<4UXJMkgM zNm7&2WPC`GhleU#w00T&yQjmr0B8Hc`?k+kTWq+2oV(H6Jwg(q_V>a#=6Hd~-M+OMgb5Rtd~`V+u+ZJE}=LT2zR3t*F6b7sRx z@(Oy;@PfHW%pJf*KXDs9zC=u50OlHt+Z|mRCKDPU8p^N}L2Yc)=dbv(P*}Q%IO=HI zR%{GWUyjeMyA(e>C_|*XO?*Wz*@9`ABl0d47(6k-PBRK4jTew4cC{P;N zUOsp`>ok!7S218q5c(DMr_{wW}K9)M~5#_w%R$-6iXNZl=Q@_4Ukz>~>H+C%?zmh>hePf6 zm<45hWnz!*TE6||Ww2ApZ`-VEbBG^*Eo0H}X+OEi%K@7guZ?qDR8@xP#l_9mq?)RC zRYagwGUDL}&L6LsB`4xGb@h!k`c%zK?%l<;Ulxr_IX!KNt2pPWkM(0MmYhtTvIMEu zpQY@H6f^kF<6X^QcD3cM0Kvht{5Ki7%gAaN3RBtou4Gogc78Saa5?1zj`qWhRel5u zMQjc)l8BtpwEcLj_E4t|cx^S+n18~AC96LC3r~5ZFHfJrf>_vk8($@4Ypg5(x)dG; zXL{J1Fc>N$1GO%LL!wnPR%RKam3ak=<9x<2%x$r-br7c8*IC$BX z3B&ZO@n9VBYLbGXq`Zt+aTy%wp`3tq8;(jVgLs4s<@q!-0%g=3%4^B?4`K*knQPMk z`i81%f}#ab@{{dx6i=tO;w{t|_nFugg~c8<*oC{#S;k`zs`w4F=CVUOe78cnp!r5w zXjCFz1Py~1nFT2&Y|FcX1|==&$^JXe4 z$z1~O2{{E~1cana;UW8xuA5D?m3n>-+t{-Kmw*fx?gG4HLvmTlcHI}?V_(kiJ9vIi zzQpWIxzif(=)XTv$Sj&otJpRk?|{x+DS`}2-(n*{-8FS}xnb`I5n;3eltotsDb>IWID zh>9bqLaP9qv2bHzlzti}0J5(P65qoRel%uz{Up z&jHy^F577*(nu6Rg2g7XH1$4pMTgWHhKr4y#5B;Mpc4Ybc8x;`@48E1I+;DCNl6sO z{5czR?`rt~jevbC9ZERo8Rkx0y6b7E}j%VWgcr(#mB zanl!_`|no4U(#jT=4yXQ9%d$h{M?`}X{Vd>;vy~U=rcKCrsOf^{7G+C73t+Qx62q? z=2g0LVU10szAznY6oao5VHhxHYp_Ij0O1sQ0GQfMW%WIyu-!F}7B|}0cgYB5u!y5^ za+2Xso!6YHf7w4J6-e9z7`G@@{Ue=(`&CedXPx$?>Cb+(gxZjsIX3pt7H;1gS3>nc zPgydr$%jgkr4T~ zRK538&RBUYkL9vI%L%bc@zu~Tgg+HQb3o-EnLch5wSK9)j=Mg~an~CCmJB$Q{bJN{ z2P<1P+E6z@-|sFJ&4TN{;pG{p=sSaHyd~R4) z&#w#b7!WRs^x4vLdr_93wgvn&xibi_&S6NJLq)@Qg^dnVEG?mfUC}y>MtMO}?vIGm zu{`mce zG}}a?v89F;gAiRI4t^~*JDGn9S~5aHNSX0;l1Z0w(19Yq&>LVbYvSa#t%hqQc8I{U zdGz8f1U+a}sE;@_)R+-K1R=a%KS@_;+F+5y?q3jQP`s$BKib9>SN^80F0txYh8mwY z!XMGk`MMY}0so-l4vhNo8z>_+o38kM*_trHh^@SGh5` zL=F~maoCunVJC#S?$w`?U=-5H7x4&-l`=8o)DArGjYjr2NmW&)< zI-&XWy_0s#cJa=wirWBps ztSB)H!tH89Zved&nf8q}MCG`Hr1!9(hllOWSZHSYc*f53bX?g+Tgi{GH%(U=a6DJh zqI|CMj=bHQiQFcK#C`Cp;@BXMWCi8dUxU)J4`;dPV~};W)N@c$LyM3)=jRyfKo~i& z&^3F^sWD7i^!^4;5{V@rhqod;=;1KVjYJH>xRpo!yh|C65)d1S7CkrdY&w zwQb>C0TEMdGUwZ8q;2CD4nU)rur@F;OW3#`x+L3f}v_OcLPH z*{|8^C0ZB-qx?M*`s8%oX;aC64&E0aAE=F2|Be0&OE^ptjQV|(nlw=2wI`I#WV#-WoC~4fhWW$kfjngj9oPP=Jxi z{@q!$>64PjWcv4^+oIJ^U_So7@|?cIfKTbvX?Bg)EJI7PN~&?1u$K+ZrKa(xsXaNB zM2I`w15`g&&@vwz?Dj8k!{b7jem4aBB5Ir%CH z$t<(o$@HSo!I7Dv3rW4RNvoI#_6sYr0I`k(fX6?fuVnWbP*_5;Xc`4PsBNYvoH348 zgvTNGPlQHfFKOXz@75WYCKwCW@p!55i>qnGsz$coJ#l{n5!1nqqV*3NuETw>?|Lrz#OBXtmYDxJlP#`}Oji4hOO7$+!SnLOxSPv9L_Rr?N|T z$^X0=OHLMeYr)N3A)DZGWeH%EPu<|Ilpk8s!vRCum)oLTB!KyTT4fa<$LpPB;@o@e z43oy2EHxxv?l{+osgL~Ql$s*Z+?&Z_m49d%QMHi?cc+;TPZOij6YMSG7vTKYw>=CO zCWLBCo2_nX3{L}pgQlPPKX^V`39o+^8W9NS%-5v%8ZVu`6zWycty;;W$$uzAIgeJ9 zEX<(3u|OJ<``yEspAjW~E@$5BSuFKy$c3J21$iY@&{yCz(#(76lh!7%3Eo~Jp|)>c z?F7zrBYRgLyGg%5Q%{r{*%S)v_71PdV#m?$EvouYgvy1SlyD9@kACc@{ejJ;@byq= ziApUo;XTQ>(py$|qC2%V21Zx_n}=GZ!d-K)iPRHW)@ipNm|hhXMz^5|ppe_~EWwO7 zPU30uYh0QxHLx_y!c9SG55 zC?YqK9+^H*E&F*|G?)&Oj&G>*gENePNjLP^%h&&w_1%aFu@i2HS&lkF!e8m*+YmFk_XKFne0gDh}1-^SjG z{*q(lsb|hoivb=%t<{9>Sz6Owr$jMBmKs8tIvBKTTA7V$zNbH%z||RmJJ&B#@atFJ zfhKj>j2KgjloN^Tt8$6=DW>r4ID$`Nt1fK9{po@h{kn?$qWuZ(PcW@z6gGOv579DEfl4Qy5k& zH-h;P?2nRs`6N@ut#5mWx?T5CN5yH;3TqFzxwU7K;%(|R{>l^ofc#yiu5SC;p0*1QH6{`1mUXy*X(wRAFtn=T+~xr#qGFBfN{AaXJ2eV+z5)+@ zCjF)<{{)3#v0{=;nP+gTi67+~y^vI+`%(Dp1eGeM%eX-t)S`Pr>n$L&G^lC*QlfFr zSToWEFK$|QtTU7))$^G3+H+&lnp0kw8=sfp;mdD5O3R?&=VutOYs8ocTh0fe0lW!P z@jF$Q+q>glJ!<^SnSL2jYh7MXg=GQvdUviK=6p-~sbBsXd?5tl2{t5Ep=H)*BDq`1 zX{iO;q793O(n~ya-Bd39dt!%IXJ)6TcqMEV_Vk+oKN+I$SiUJzwyi&>%#6TU8)O1R zqJ-XrK`8aab27ndMl|n^$&4cBopg!B|LXOq23ahh@SbgCzM$G+62&O<4iegIy8 z{Es^?SnO^S?sNs(6L7?tAV>!;`TfU8y~o*w!{+{a7E^&PJjcbdBZj=(Dj>k5<*L&i z(SsJ9gS4^=M$vL099rXqc)Tj~oqCnj*{p!LPcF1gzvrm(k>0IV?{>&{#ImfSi%X0!Dqtpan$FQ+pPZd=bCcY|VY&lP6hkSkOsi{!6!(fuebT>5)(zQx- zv0+iAQm(2Ae%R}DP}A9_<-eBLBMya%X^UXqF>+RKW>hjrV+)+2b0Zg6XU5}P8&{;j z{ijMu3u3oo=G#-ZYfQTd%m2OMc5C{b+uyjMUvlfpB#zISEf$x7y@~wUG*4qcxDm?YqR*}0XydLp0}e-MAys(fUcc)UZ;IS!&P z2%Jq057(#aP?#x^P4jEw++2PUpRDgq13)g8W9^Ku&i6^#RMy9JoiU=$?*=IK31<$T zco#O;*RoLHb{)$U`_;KVo_V>_G7jvYo%y*WFfYn9NonU# zOfK#flIdKnxBbkdM%&TSJSlE%jVFq%x2QITEYX+*$e@r1$8FQZ|4U*Kb?WE8lNO;scdUVUNak# z@u`9}GY1VHW77_dr!Bo%9pY>n9NRBUUr`+~2~B{|mG009pp9qGk=eSj*09#W8MvDq zyVh!Ebuc<=kmPg9*o;%iJ!^_(J$vbd_1i(wAKhTorC&(%NZ*^PPb|X%6y~`K1kNf# zz7sI#Ds}6RX}PZf&EuXyf;F;oFOOF&=SXR|BwfZjE$XU#Q8&D8y5$T?pY9wAH_Y+% zGZtqilHbBaJ(er@pUYw}d<#Wtsej`?l54n!Jx%nJ3QU?%IODf|~wG)~tWpH+Dkc~-cHcd^w zi4rMBt#r=_gB2xb{z7lpQ@h3}5Oi~6CwkS1s8?ml;lyfj5HGs-VQuWsTBkEsCjz_r zjJ4AWyNY%cM;=&u2e?;s;n>I6Lq61cA9m?}mE02ft7F?d+$yral&8u=@-mz^iLc^E zt`ta2j#sh6)a3p->+YA?0X}~w>wq8xj}uGm=UnTSwD?1lM30w3Fw=mxEJH4SMH2&- zEhQHXW22oZ+r5Xd_+)wojNi3KhQ)h@DCV>M_h>i=jWqyzz9}Ps9$Lk>QDc($vOCmx zlN9{7TUsU8eQdgYckU&tDu`Mo_J?JfFdf^;al>1AHL=KS>3c*AxoM`q-zbcZ9P%Pd zRX^*OhCab3uO1?Q_6ziG%|QbcwQh@ReA|=?dSkTU-akqdoEVi7xEP6@Uy3NQV4(>< zVN+g)z} zi<$?)G#c+7^F{l_FHQP=FXy|@^0jKxNS;u1@YF6U6Jx??TEVvu>=ErVJkF8yT92!$ojh~?-*K@6(WqbGQx=cOX;!3*EI`1QLqjMJ&ysO}qz?BWs~ zoWZ0lsJ%h#p1)ECoKkSJ$o`A%uelkdvo&jOHvScBuk$>j4DrYUcl2@=@m@6JZC6Oe zNa9$j^kTrO9B`N}KI_FRgaX}|j-SS&`>`=mIrZF5gC`iEBO)LuWa=u;N76A01e|HU zHpt3pD=FL-rR?DI0KpVs#s*CHoxtLm<2nBKF$f<5I&0zpLzV?Us63DkWkv5hFx=oj z#NZEovF~&AtFod2m|X}$KloNQpq-kNWkzud<_Qm?)%7WgiCExQXVVeI1t*wBXL8o> zI5c72F$>=-PAr`zsrdGf6#4q?1}I#{{!}Lqcs!(WL#+)F3f54&ApG$Gv#X0&;?=%GAL#cL(p}PEAn&oZtp4|L0Y~scv~w&^ZDKMpRjI0 zIW%&^JiP`7X4P0pLdW!B;G8A`4QXMa0j@+FXo_xT+2~l%&B~NhL-3eoE*h zcC(@5Q1YLhUD8~r3x)C|UYdjS>f%MckLI00PfuI(IENd<+%(-QB$GbCtfNfn%=(7V zLQxm(Z)dOlWsa3mv5~2>OmRZC=h>BwtN9}Ga*>77RBtWhjT-vzd4`-Q4%{3{JXDn_ zTjp&<2L{A#k69?t4~wldDf2iFCe0aU38f1{HIOLZw@QnxY*Qp| zxxrEZN5?*P4Jkeo+}e@%!e`6fx7W!VVY&hLavF z=(#a>b<#Hxng- z==h@nI)aWd(bvbj98GVA+hhAae_gF=q*Ff+Z`6PzjrRSlC}Y5`)qE1F78NLSTFs@zrH7QN={=|=nUuxgEbs>ah{y-kjDZhl6A$k=Ob1_tn7+7@83e(VQU z`l|qUAw{BiUGpo&uSN7HN9bUxZP?MgUj>elg)r;BM7}q$PtqV>b++kaFfD_l7Klu$ z^o68q9Lsc663@^-XoDoD3c(O=+mYe@Oi)LCX`yGU8?1}-BQdn}g5j$jeGAf*bJhi5 zkAa*KkzJnn737a(^WIEGWPKNxnC(U!n1>gV9!Q$NK6+&9kzSsq@S~-k7G|Wv(c9Fn zn@HKU@l2({r(WAs$9~MC>1vp&2mDKpxj7N&VcB1b-qO#NqrDbO^o9gt2I85kqrZ!D z$jM>C3(>3e6^4s5XY>g!+ARptUO8{I$Vv(3!8Wl2jiun;4&4&OLDlhVVa-&zqTJbr zxkgin@0uxc&Wu3x!2t8FiO+31Y&-fLTOmi%JtCJE(cOblB5%E#9`K;{9ozQhgu_st z%KRjP178tcDpNJCFB5_*Ub(6Pnd?T}6w?Mi4EXFb7>`<%uobHvpRx_bolS{Sns(y9#Q?_GG4AsB{wJ8 z(mZh)Sn7WS{BA>;+*@OT%o;+Y46{Eg#w~{Zg#*4-1#b(6YViFI>IaiM>5Srv2U6Es zJ!NDuPMaWgkdRU16}6)dLi^&iL&1r>k>SdV#c+0giKmu@?^4a*LIX;s z085>r3|`?{i=-HsBVUkX^7`N|rDKF<=~)~3*2e8=TXCYUmzC(q44yKO%}~;xVKwFS zU$lX1v>9Cf89#_)#jk)RIp4M80xICOpqn#3YbW&yM&ddd_h_uKhoYm5lkj~Z*!gK6 z68`Z&0n=cm6yud9q@riB{Jg zf9w~xP-xZSDfP!*Dr7#HC7AHf7v~S!#@gR> z0AR@Xgks(T09a2pzKtseCTK(M@h6FiyJmEs`jdzsN>OiFwIYiYSLNKy7b&UWB+vr$<(NVHY0<~t_flzIKS4uJq|Gm+KLt)V zVu@*;(%nBW7T6gS?V7T~n|WZUlGw&MV5B6UIWwI=z3A^5QF6F|#O1l~L`!Vz$flbw z`)~gC5uD#jsEW8LA+@ADnYRaT=Thj|^0%L6Zv<~ZfiBc(O*GIEVziluo?qxg#%Op` zbP9i#$IJf+On;X~sW`P2#PV(Rf2cFh7|?7>PWVJsJ&jY73JC8uy5C1)(?{Bs2BD68ABZ`sN zCs(LgWNs2{a$d4FYTo$n04~&eikf7KK%E2woVQ8v{RC#l9Kwi{hW=k4<&t~9oKv6P5+WXYzcSdG^j@b)-jKvtDN6+*S58}2I_~A$` zQq#uaMzu>!_;QzZpb|}H2yfm$i~o>T@6(tr5u=b@2?^SE(f9{7N})e`^DtNh$=kSC z@q{oC4tK{z-3yr^{NobvGp`S!8*V&e_Sq%4%}Xe`qBGvDs0Yi-3j5OSt229Bx%9R0 z#}96jHfQ6{CBJ76E7LzIBV?n4AU>zK8~EDm-K|UA`Yr~J>de{uA^=UVia1_a zX6XK|LQF-W*l;%yiyotTzc^8;S*=1%aiP0%1PA7C@-|Kf-(D5!=j0fCOh?DM_85re z1Rwx@!jh44c{;ufTDX$$S_r^<+vuPC!_biK-rj&gfgp$S!LgQU1byZ>^Po_zv`k-&nE~9;qTW#}RjcQ775{SKAz>b)16DPw zRyAAZcY%0CZefzYqybEpJumglG}{|*cV){!$+ z`^_RcK;_O-uu3_&c>gcaV6!|?%{TjvRCKiT?xxMh)}#U8 zVZg@gWP=S!1EvO6)CMFo1+}W6?tCn?x^9iEeJ$a}QiojD7n;%4fVR)a+Ee{BEZ@d< zHi=vAdgqN8_+>{ACHfWtBdSZh>T3!wYqnV8Scr3|B%Q1VTz>6-i`^=hJ54?SZgY~^ z<$APU5cc1|sam9p_gvHn3DaY}x!m>a$QChMF2y{D$+qfDKVA{#Nal>-8L$HHXLn1a zWp^BwN1i;u#I1_Uz0K&4d=c^kz>87rnS;Da)xwO=9KHvmrrk%11G7eFT`jfEGOMRyNSmG61YYLmPcLD zcwBYyapNzZHM)XZ-RJu171g4?s#O|aWciSfAG+a_Sk>Hi(ug?HJab~~`4O`z4^J7X zUB#IBvi_+905-kfqqm$|zQfJ^X>5VSlU@U=i9bWTY?F0Mc`AogH+d*EBK|gI#vVRj zo)seSVii>Oq8RrpOe^4yttKfmAr|Pd5Pl_rz{hbS^Y|qyjIOmSB`0g2cQp!a=?!DL z7{Q6k-5NV+YWBG=tr3$6&{sO|Bnl}%dv^A8VhYhS$_ym})v`!^Aw8ZcO@3Hdj;kPz zzNPfCe!!Y~JtPr3V5)mmcQeQ3Pg^z;6Jr(yc+SQq;eddkF(U7!7mVrC0FJcAM!LRO z?T+jvQC{y#De_J`q-ysGpPh_xw6X4lde*iq`Sv$Yqnm&rA+=Z8;~Hh(hEGT_zGVyl zL^Y_;Tfg&GIyBW5U1_tmj9nX9uQIB5tP`Dz9DG)jS zr#aPGSn{R=IxH`~!GNu-51B~3gNxI8=GD#4-u5?4-<1c-ek){8yF{5D95%_A1D zS2XgL528_v5N9R;j1%!g+UjqYTQ?Od2dX#n-&5*Kv59>XoL^UVdnMn6)nsm)tYv$H zQj>jf22?p$4cMUnk6?;EyAA8Vbs^N?rgPY-(_bVZPtH5gJLS$&9;=ig`r^QBL`Ch1 zo5U=A33DUXAV1XkUbS(wn~S{acwLnV+Lsoz9B1NaH<>8cX-0ns?C9kiKo)=%NnUE` zZh1PcPP?u1tG@q_;M4qLO9sF}^?+$_6-3i4bLPJ2T=;lB+sbQo;OOUDL-&{W{rr^T z_+SgAuS56^ZW85TDTUs>a7R6MXFZ8=1#eD9HJ$4SuB+@1>jn{<>51912OeqKNWO!D z`UEwr<~dl*O)Oy<@@|dTS@&(HgOZnX1EwG{G`dJ2*DaR&_wlfSPP8=0(ZAn|1*QDu z!2a<~6!bv>xG$nDwb02=b-P&ESs2Ana$B7p@vp2L07*et{ICGUE93Tr9*4S=WH}{b) zbWvX63|2i7Kc#r_kTWz`UinE;IKA+vf+EXkG3y@w0eYHYS8I%D5&{>gyt|RT4pgk3QfnV7fH}>{DM^ zfU)eclC0BZ11UUSHRcg@!^gdbsy)qMM72PkYDZS>&rnHCjA^Q*~YP*+>B#JbRpphAs=-w=9L_S|}v(fcR%mU%L2-$}dmzRH4#4QF)K_i-f;}Q9u ziGg|}E+&Tof6Fw8OGG8+X-RLr-fz!gIjy3XKH6HYwr~>Ae$-uE$zz~YlZxaX?Z|uE z_&xrJNB`#{^ywuTI+}a4Ehd@{btZkenEY;_C?K)p^hI6|t%Ho=ye90;&}+r#2Jq8l z6(tc}4KI6w@;D`hn)rzyJr(man3t0*}q2p)wj3l)q?XddmFKsl|J5?1+OL zdz*TmEQ8rI8Q$DdvAh~3()J6fJ&!VAaTk_Wm@IWEZ_j)9MawB$fF?EKhoPU|AZYjz z4pm*2lV^=3_lEj#QE~VXXvJK{Ug5P%hk;-&I%I49&$O+o)yGWJ`Imcx;!MYdGjNPb zCG_)p+GylF?8$eKKx@&7X8QUOIaDgLHjr}KT)If03I6<0ZP6XH^4t9x0)_Ide9LlBSj9H3rbQDT&|C6&(}){s}aD3R|ASu z*cL+hc=nSKjoss|R)Wgm_;>pjEh$i;eIBR(aM+s#K;1j<3c;?6BQK)Y*d=}gSO}_{ zn-`PnSbIB!+25yPSlyvEe8wtVCfYyaI8sL+uj8l_Nz43D`#s!trRtF`vOB8Ksxv4g zs*#P$GbJtTxN}b7+R!B17I1{3!a900FMg{$>%6rpi?#Fl(7ZR4g{yk$R8^VRx&!G7 z&z=R*#;>6|@!rTa^K^X^xmx}io}x<>#(ZApLbSqD;vu>grhJ26%u3#8q|0KmO2XAZ zx6kOKl0>mFlILnu5l6w%St6&&z;^MB5^t&eKm&Lmsg2RyHLYHGoZ7heq!;iCJ4Wud zS^;=!C$`o#Zq}eA%FT;V7VyTHjz797{4J}mA4oJDm$`8>23s}p2o;HGzUdwErlIQy zOMf|+o9x(KKU?9rQf(G7z8vt4oy_{#--OHU>deOE#eddO>V0W^eZo%&qd!II{|v|; z_c?$cF?El5Q z98u$E14VG1<-Xx(cw%E9!*7RUd+YaY)c0T3ky|b?yQn$!)YrRS=rpCE{)9sN6*h6U zIE}GrUe@Lzm$R@>u2u#R{ah2bXzg7m*_B!DYo4!5mxBMYD$jo(8d+5Fb~&2v1Z_v! z2SSz4tX&SyW5gjCtjMBxo$9V*Gnz;`wsxwa_M%jfvS?YZ$bccvE7MK=e4h*|6l7@) zGdwRS6l+?NsNHPSk0r*-SSqL=MnwM>s>$-Qn<;6GZf)Thpr~Pm76~}HUKG7cgpw!n zj^T3H4xw`A`xhQ%@_up)_>)%tddpiuILH?pYf)9H|JeD^>0fzHg=F)CIBl`Q7r@{^ zYxo~dus0q0pZ!_4u74jV_pwLdGUM5+m%6bwj5;+yS~`$nWPb1jJvcW>aW;_FMd-yC z%aXW^fro%O^Gy%%seMn*nU!C?eI>rP;W;oWBdyPa>j-WmgEVOn(gVi7u-D8`zQzK5 zRsAxGo-QX*dj6PH^-;)ivf5!Al9Ven<8GlYZ}X^b1=Dbx`;@slkEGLTzXhZCBRCrj z(~2TeBGebSV!Q)nZpJ|oTx7H2w`9x~DwX@Nuj(~jHxg!!G>rR-9IfeKoMWL`7`)Xs}-~COzx<*NRkw|~G zu%N}(%GP#Gi3~kc$Ti~^EnlAX^-9+FCFx-ilz%b#NNtlKuQ2IMr1bpMN5Qqb9I%jw zxHavzikv`E*shJTS3^CY@*`n>(^w;y1yG?ar)d1Z%>G;=+{(muXR5yZb*0b6%!mc_ z&OL9ZG$m*!Xrd0QpPNPo8DzdJViZSRr-2u+^=jJCdE)Ud{=zmrHu0WH7yJ_lMQo-M zlf?=qpE-IwmAe|X5I0P$e0c0M0NWkfO8!iFC&c(QKBxYQ)C2!N99La@;{5W{1XeEj zG@X)aRTuREg~KIN*DpaoxCnZ_Lj1OygoBHwr;Cuudaf6{9$=?u@~_Ft(UHhx1&Cq+ zpbcJB=M)J}WoH&YrOp5+a9b~gZC}6!ILo_TEcAo-xrQ~vg&ktcP>zHN%EK5$l%%U; zaeyn;S*gv(NR>x9tEfi%nBA(H{g!+{JejZf&y(tJ^ozg?j)KjZ=&`sbe4%SR~$6aT69 z?MYvhyoeX_KGOLuYl>zrGOyJVw>m534HMV=YaBgX+tq=}Pm;E~!}E-vci3-$uAoM(XqJ-7oJu88PXdi(8Cd&FCIH~G0qWr?|bLLdI8s_7t=GhBJpu` z%+aeV6n1wh$D`#Xxyj`EPxb5$s1rHVhyj+Yq`gKThE2Q(CM-&Hd`C0Ft2J%SnVor( zD3=nBVhc3-Vrt`+{VSq)mNlcBk=%PRgz0Y_W-L#>_^F@J%z3;`_>9C&$@g45vioBP zSb3aJZ2h!B=#?#`3b4gXt~L*&`&=p$qE?=ICG-AG{F@z@YU(5o3ybtY>;EDWr7dmw+S1cs z+nF~`IpC29o%}l*lQ9EJ+#};`jUU;349!8NVD$H846=D78-88b8kpX#LzL6+>J3$` zax_0ndOwXU>;wDr%NhboOu0X_@V4L?YA4#5YO6NkRkU5^b7V5(N|r*XBF)4GHHkBFd?(Xn{Hn(@LJUt_*=|Wk#0+as;w}!dp1t;fR!<9# zjZlpbxY@d*<(n?E12rvZT#W)nhKnm4gss=0JvGu8#aol!NuDSSW5Fi4=A!Y1lpfQaD9cO@A6+2I8$s>x) zQ>w}8kIMjiX+(NLJ%TpXnH+LGq1Iu&pBUkea`}i}i;S=F06ai}sH&li%_RZ^k^C3) z!9YRzeP%N!cabew1Y(OBqU09GYtDMCZ-pewndPhAj~{v-=~gLBlBa(oa2=GhS}oj94~uY&#Mw(G4^FWm?#)6l^U#g=eBVBZyK_Ed^C@Xci#=#6 z{Y$R+dTTJ9awwfcuo3}G{+ORLEYKZ#`DCowq@kGn&(efGhgS^IPlu%mW`i;{F4l0x zaH5jY0ZL_bgc&hV#$%_Y5m+?(LZQC#J}J*@CAcU@ZX=3D;RJ&?B)xhbP8*z4`VTsk zze5fdfhzwY%keXV?PKs+55D3uXwz|K_%@yA?>lV%ao|7dqq-HCh}es- z6Vt#p9?z*SR1%W4fvz@+s-}|mq^yd|;{!R9(Pg6Y@+;;ByR#pJqGt?s{0G%z48b}< z(WMsUSB30B^14q(bpVi;o%v#5NG9C}cd*G)pDq{Wv~*6$OU4O?E!zfQ)Sp&@ui?y*gdg}8NwhnyVdMJnIfC@vqP>60uXQho~# z8o@s|j(C!@b4)7n#`CR7D#9Sc_SjB-|1R285GRHs0a>OF{`Mu5P z4w$;wk?P2R$GjpsubEM!fFQGyUk4TIo{;1xg8f)K;D}2cy6ppph|wTFu9gW${v%|0 zgkWl;&DW4f?uBoF!J`PRpv-!*_9^bUwp-Cx_J42!87p9NeygI#O-5bmTa2MwNOX_3 z>fjP+BsT5XaWvjV#0d=M@HOTpWOzDo-Jw&Qmy}eu2er!Xanw&G|0%*drnh5c=IU{) zk>Cfk(T>!aHT0`2p&y-sEN=qsZryVn{!gXYZiLJF6a8}IS!HVR-a)tr(Z_dcI&00P z(pHR0-D|uXY>&hBMm}rWjSQ{spsZO}*gXE>Kbi8j=2?&wi|x@$dN)(`YT5Uno8&=J z^A3OagXsnRJZV&UPOz#9Z>k+lxTj~Dn4&As$!~N~Tx4|) z>egP(Gc$YdEtQhiE+8HnZC1_IBx{)l8hOKCS|&!E|8o3anW@vs@R*7C0QJXy6dhQD+ zPQna2%|$9(!3ehbP4=Em|JRtT`LSS|1Sg=3?{J}x%v)X^EVXtR7jiaE-GI7KOR*Fj z(}T%g6jkia=cM|a6rhDx&HMX~t+FjVDlcs^gzIS39<{Q5iJ&o(7#>+8*Ny zcS3edbBfykabyg%4GNF&+DQk_)s0fgPF6u!&R)b=ICv;4P0zg%UWTa%r)uqvmsq}c z?W%t1=Io@pRL>0;3t1iqaKwjmm|5IP8cHFF%(py#1ZpBRLGQWQx&FjH7FU%x4{H{0 zuWr0B#u2upubAf~J`%FzMx`9=RIYk5%fKUY5wS$R@0On{Me_e;u`W_kReu@Irexe8 z?w&jYa|V6U4$T+%9 zUQ8lk=7>}>4=CZ;45w67>so9uwR)1^U937_WNmu4q~~Z4B$d8b5x+I!FQ6$j0Bb7L39Ee4)O=o8BdkYR%v3)OPoZ4@N3-p4%Ooz5zMOtq_TKH_wT0i!32>eoWBpxin`@$^CFCy6QL#eVlMT33{ zK-ApKNwrqp_7wr4E<4e482FH*50HOI5ma40wZ~XCn*!AD_02ec{w5<%wR?KcDa4!6 z>5YM07;KllNOv%69GBx3@1>?~t|MLXYV)!X1N+V&;NBmlK=r* z3_U*I=VQW@OiB6l&kDgnvG&%~1XpXk6jA(peIQUQSJeA&w~M5IHnT-z>4|2R<~llO zb`V)zR*hTNH~lVC>Mva9HP(HJ&~$fT&p1~zCf6v`d@~O7Smj>4Ot3PKtHJT`rRs~y|8RQTw*oJx zI=HMCu3MgtS8Tt0;=M7xF!jc?8A%EI_+7OYp+n+4@ut)XmH0dALNSlCuMLnV_^$3x zbm;Nrr-VP?>#0>woxRXa^zW+E?*(tD9q{($n|CZ$1)tv_f= zMf&~;ZCWk=>-`T$NB%+ZKyx>>0Ld7BtA2gUMGqm!$xPRjHt{>K|4n@#%#VbIFQhMp7afwwt;Kh|8OYt%g(g_K3qIeQ?vf7 z0<@$Od1Y^#^H%R7&K@@vT5a6kRbW}XKCN_@HHdVb|Fv7|Mo}$a@nV0qYfu;{$A)O+ z_D>}1{{9c=+ts$|6Oxxl+wwOdNUmRXI#+UK6^9|Z2LEqRvZH}N>)|1g)VCY4Y){Y8 z`VRx}gJ44WKU1|_SJmq*bF%YOY3jvqH?#i3`RTbhHFrL#>mMnH_5Tm&VSn6owD`r! z?Wpz5H|c*xf^&>=f8HkMTi)d!x{CbCEz4iszh@R|N1>ZF$7$-v zf16kTO#%MHIs9_^=l<;G-Q9(}p8UtU#=NdWnLihw7Ou}`JH9x6THN;fB>(n$_vc^n znN_q(`70-J^s&=*qQJyUklW#_(`Nm}w;5knoPHeL1l^2X)wz5iVR1QadMh34hl=Vl z8EfkKRkWsKIwXDB7cQI(KF#lWIO*Giw_DbB56#W%fIk4&;nef=SV<#ffF0HNgd`zeozr;P(`%wCn>y)#Q-aNb6oST4(6G0!iB!W8t?hyyC=T%J_TQmCF))^ znf@B%^n(@I(p%WyGHO;n&mRH7vWCOIWWth)KDY^69GDV(9lRnlZWJmA`2EU)FNszN zN2i9h(QBBbUgR+T+iZr2aNCvM@vpf5aC*g+BpgQjQ#f_1Pap#3t7)4&8`mECF}KlW zL1%Q0d;xaPpy7v}^l=$ypPIgqGPbMBG3QpdVRiliFxVa~mqR-#j>npm{fWZXemN_^&=O6;DoqqIhJ zCbNO;;BFqC){CO23%&6rt~Ew)y%DMS6IPg_Z&Oc5)fsHKu_SgIGJ$DnxaWOYVT5~x z<&UBQAU17RP)+?P8p+-#VGETVR@|CkQHZTa3p!8&+qx0bQ1V);+5;9dk2%5DJaaXJ=p6OD=o%l@iF6D=Kw!dAlbDi)F55 zDQ@2tpef%o(go&}$z;wr=;oDJDONnk7%c$SgasE66QVhhVWD%N9qM9jN8TDNN4tN9 z@Qp+xe9h;=V?QT+c(Eu(fG~sd)~fSBK{V!%svNTqfwXd3wkWhnrD`_GxOfGtmLNt6 z=ZAlwPxaFqr?XSWeh2shNxWh9zz+(#T(ld*29NyJKB)H(l$^2&pV%M71q9!~gU9{e zx#8nEWWrp+?O|DyEhjxC6~p*%`peN8wXP()MD-$`!sKr$5;+$HnHe*-KT!?e2Im~z z=@m&?C{)CP&Kt6FWO;cEbKXYmqq)^H6&@z;j!@Op&q<6NxKJ zRt{sZP>Y7ob@W;8B*}HXnD&DAl5LbxWzve=&}q|WHQ$C7GBb8~%VZNZb&4w=#XZeQ z9e}9Ea|Y6sTp6SHombjq~o?%1T@1Gl)4m_-}d<97N zN6_>m!EUBS30)L~g9KIZJS^wSXysbdmFKU7cQC4kE5))^Nj`J<c;f7X}QD*ii_v@2 zCia+&&jqv&|NQ@boAl4%9-^EZPc$;$X0@Ufjh-*lIc0AEHwo%a#R<3!jnYUeX#M*e zmPnfQI=Mp5+K`LNrhbt*ot6cBWG2wF+?^ImB4hqT6&A-qtf@(f z-=*A28yZq(kt@0$q6fqh?hy3_+T_+kbb$EXUvmp{NPYxTe+F$Qt{zwMhHkl)N=kXUs-r6m&)_M4aJl7&LD%Q+ciuLA`1ystu#(0^2nC1C zPcYW#V021^k687$+n!_MJ$U~V0OTIl)GzIPSs4$iqARBjkw z9p!oiG*(jI-iM{&f5RrABv2Bd!Ye7{+#X*RFHbYa6dGAy-CNWt6h&%ZA+$btguU<;%sxe=KS%yNcX4OfF0VpTXmNPz$N`-F-Mr*1*xx5Pu z)YN9$uFWU&3NM22t8Ww~|4WW8+3!Z~=(%N?xB=S3hn%uG!Z-u*aG!vk>$8U^>uXn> z_#6GmKgxs3=xi zy%RoZCzQWE0@hu3Likl%a5=d-4DoUC1s8sLWWnB81<@#qwyK&Ognk$C`P-lh?oBPk zTOrW5f-;n?!)mlNU40J2+xlrF4OIP=3j8=q>As>}%lbxC@SycqB zX`Gcb4bS-V#+rK#r5o&xe=-msP3|MVJ~vQfU$#=O8(CykL#y;=26O5Gn^GE`7y;L> zry`fzR_rR^LV(6I0NaaT)oIR%71uR;f6eF?JWHA}#g`olMvM_K>~z7+8?k~&Q~do` z5?B|mHAQ94Y8Uo};9<}*gQb{simrbH4;#=cEd|W`o8g%EK6B%Hh~Dp15n<-8;aYHAbRDMU;dy@3@=HV z{m2bcUmk8w7MR!k*1f585>f1r1I1sayooKWWqHmWa?pR#K+aUs2<6jlfB2+Tb~L{N zm2x+iXrP4Y7RNHklSmw;T_{8=1z()N#~WHCSq>rvI8YcrqMH*|0xzIObOY;xnEE^7u^}gTXr#Z90gYhJH?=uxm>248B!W+w7!rQq?6Y~Dx&qZ z9n#H=Eho!rH57{1U#u%emcZh4Ii?GG{`IuYj7eHaWWUhhxk*il84=&qnHL@k&9TDZ zzx>sN^0wgYRsnN?`6V!{!2EQgS#fdE& z06F&WFagtt`wCg1364`t9G3hfV-esvn)bOstGf;zKqR*fR^A`;^JzRoj=!#b7_wy! z2Rs>gmaUZ_nuTfMkzMLMpAI{a&1rBb9(7zxXdPjl(Gq?1Z@D`Nn7FP3;N{gxF$uPg zplL6%+t9BDtSbL`6#X=@a7TnPsB!Zdm{dEOT%XgkjIYP3dC}$ZN;DS_pM;UsG0k?J znMl(p54=X=8g`d}$>8?76A*y^1$-kakumiQYT7$znAHGbT=VALjby>I%vUfhVp zo9IJeeOVi2qf=xVwc&w7gb}+cUQ;JW_z!NvpibP8Lg)KRMGdA@@xYDVvF||mrbM4d zB1sU&de45It{B<+Vrq9Fc9Fz9Vk5Hs&jh6I6|?(Pz_+>cF7kyB7}XJ?0-wYL7XlW3 zVw1EJn>F7+^GZ!BWD{rA$6Rf%VJG~=B*yER)c4t@gKoO@T+~n}p>7399qrt1{9}~lR=J5^9<&9OC zW?l8=qGg>=tG1Opj!O`eF0X_j}`NkKN0IH3QdrEZi z!i$bpb;}d+wxK7v1E=H5>)w(Xh1WX%2V|9z_9>GYPA_3i!ketA&k7<*zMfL4WSu1P zO5Lg%ij^DwF@S;Pg7OR&3c5cZt=aURKC{S`Svq1WGb@vFa!}?^5#MO655*mePM?{eXxqbSR5;4WU~)9~Hpe{&1{5g? zamnPkhREHAkZ(f?%TI|Pyux}P7ITM0@RYvSs7J+p<(}Xc=rB4e;*dBSqWWMP#@O#6 z)3W)LRGa1buZYvhGzXN>5dUm?qm%=Am8_N?1A#({MVAq)-3Bzpj&a0hUD@GF0^+oq zVkD1sJeJ7#GfKs@aj}>%u=`ylNZWo!n8b04u9iA|>#OVsyIjRf{z>;C5>I~eRY?OE zLTsB;=y=ovu!+-Wt#=k7u`4pg<7|44VOkqL@8leBY~H&jeEgS4)labrQgpu0!}jB% z?v!uQT=WeaLz8Vxxv?)F%U&+r$G41G3F2C!10bo)@=A;Ogib{bR-+8u)QWS?o_R|O zYb0lYBiRGp-rs?8acjQqbV&}4guJpFB{u+Dg?*4;_ zJR@Pd**5xp^!ZM#ZLa@_%jIz^f_*fZcQL+RJ{`WpxSb>D=%%rwT{8|c<7^(1Js#Ng zT2)R-VWToEGEooDf<5j#P${WA&*tCMXcd=su(mlYm^V+4zjbn3O6P04tOG$I;jCQvc@VMEPuKnbB;DI`n6R#b5&_?MHuj!-7q zN3x(M+=9bBa!0}IxmK%sWzvu+es^9%^@iFaj^C|S*Z?dyFWt6lu_mPu!Yd;-zX79V zA<_bLYDjnH)tm4#1WjR`cJ1((iucF=`8-XAmjJvYJ=b`1Dju2XuCI*$hoeFj??rwx zDIgys#3_NL&5U3|M|6mW4L)0Ju1E_lYmd8DB_SOVP1Tu=ekXD2*>MAjB@kdMci?Gv zOnqqb@;z|3OOE^0f3z2g0&70$v8(|P-AC(OeNXO1;DK)WGN&d-~)y>p1knR=EyP;(+7Bd#21nN#G*_s%pu;p4b27 zC>;UnrQ70mQYF-UQa;PIld6!R78R1EC&x$apy=RWYx)VxvBx-VD89E4mKy3FJJA-O zP1uP7KV6Jf;WQx1?wWVaZ89isGMepqP)FSzjeoF>QvNsWo2EnxQseI@K7O4C*d={kBW%RXg(ECO ziA~zaQbN;?S)qz4AII_!j+coKyal}}(YwD%6U5vYk>;JQ4fGv(=PMk5n8n|F?Yk)~ z^bx8B@THkdu#q5Ly*rvOjd+nj|6P=P^7;ZDeH5Be^W?M;AuWNSu0XAR5#VR6(d7m< z5DBt{XdrDhWk{Qw6Hnu&c8df3_eA9Q(Z01RS*)<3imld9YgZRDgh7&BrRSfJyK(l1O_@8s9GAjZDnUJQ9@Soa0=wqN7v@ zUZ$qodiq6jZ>#pHqVG}$v%E_k-LK42aQc%080~%1uvzUj8eKD$jmtPunQ z3UnjIl@$Y~*RIc6Uo+>qk^;lOxv8i0xPL^cC51C9(2Y+T-M-wU_cGZ*XC@Vye!mcX z?@e3WYz%#K`Lmk;tNn>rOgU<82lg`}i-Je*7!y~n{JKfiT&R%#2i)MoU1`scWJLFt zqSRG~t%%iFU+wObUPIYtuvag)+eDw1S+$;nT|{fhV5syb@>@URGmzQ}q0$~9qfa;* z8cXBgtTB>l^tb%X@KQRz8>#IfdFcC1^} zL(H3;C#JDjZo{Z%n-_?c8>s}!UBu^DvY1@2>Zm#v_ok+jI@ znq)<3N@iaCn`sW+SMEU;u2E*(5J~(AJ&3ZxGQKn8)#MD+IamQypgy`5!(n0`X6_7o z{tp3JseIRjMrJdn9zp2EnT+qsz%i|z3;7SSKT)PzG@gYOAi4cOJ>|JjH65NpUwqI9 zKxL9?IiIMZkA6utFdX9XU3k=6_3xM89>TbM-$znVMn+Vq>GUA0>4BG@P*r_EiG-us zSs3KOtbBdGLfl}$$Sd1L*jP6-8I6*T>m)qisQ$HcH7!?O%~1hXtP)u7iSfz_GYjNn zFnROVOxQ901H7ic3LHDl=D`^o2%E1#w#6y|CMETo1|AB^-@WPo!_hMDzE~)cJ!Heb zY7FUrT`+yYRLav?1b}~e9h<-09nBd3V=KLwpU?PC{9LhICH z?B@Wvs>J5^Z-O{lyl$Yl$ za$+^Vcx<97%Uj^Rslp%tp2iDVZQWsC*MOAWfctx0$g{^>%p%_(Do+6ZTh6r-RiN&h zO+X^nZ%gH`5lK-nvZ2DJ+_)`k-Va>M=5BMASs_#jnn$JDsbvNbgs-X#v`CWxq6RR} zQ8y0L%%5Hw^IZQmzyiOtr+j~{5iz0D4=x!Di)#ycLrC#$V26iJbIq^J?7np*vu-?* zlSd)ODu(L24eUyvQ0=6OWA4grRI153+};CI=3Qi=OY`>@~OS0 zo~Y2yS5~VAY>%W6Tc#EI+GswwfmF&G7IXn8&$y;_5)I?nqKr+Hqw$iS&Z;H{w~s`DZ9f3JQEc>W~pjDS^*AF@@&IkseIk^rQf%+DOe%+HJS z*n9n}yeja3Zd#C}6Sk(s82cisTu@IYxWfK`pt+wq|6g+Mt7M6N{;=mk9_|;z1|O*jeXW3w^H$B)6Y_U0*J@1uMUhS8%uS=z~_B`1Z|AFxO z^8=;ss#wBBjc=Dao4)4Ze>lGdMLeR};)}L3L%PRm^OqHc{eW@F7MTDiVGJ2c1Nf9+ z%Onn2-eHV*zWup&-d0fq8lTB3A>kx4vgN$yEfP^#$9&(Rp1sJ^(15_rx}jL5G8WF# z&Jf*G^W~9Wb9j9Eg(T}AKhYWu$CZh8M!GbtpB2rkrAcYW@`WWJH&qxZ+Fq zZ@n%$HCJ))IW zi*m~QoXKSL#lX0;+I%uO3n4*5M$BqTiasnX9>%X)0aJ9$sCm9+DZo@7KN;;@G+y^1 zgaR+IRQlGq-h)}OC2R6O90@{7k?KW93W)6p4qI8k_n-H+XARlVT@EHR?Y{+AMyj-* z;cNPsY+L9u%#!NdG;!5{5@JScA{EfBdT8!hz^MbxT#XqlEtNL5r1)aT>b*sLr|Zi= ze$Yg?J0kH*@hf09B`~)?wN(EyYWG7y-A=J6rH=H}BjzUKcjYvN=!kn?J86MTccBHN zX(!K!qWImD40Iw>B{S-q-L$s;a6^V-sZWJaDA3lc_C&T7RPwYV z19phXZmrMOSicoJdB*TYwDjFmlf6=|r9e#_uE(Ixvy>}9qv*-X5Zwi(7K}L-oRnu@b0skDXwk`{X>d zf?V|EOR7)%kHdUKo+ewJexR)W(-X2$lMv;(iMVP;h%h>F?9@}S=GSEfx1_ zu-hxJl&AYTCn?W!Oo{(Ka*j}~hW9@9(_@VRFz-wSb6a{=hm$d=M11ho%~}!Nng%P-BM^QikNS4<(6?8VOKTwX zimQI;+c*Sjy(TP4)MU&+h}{@h1#|SCT=1TyjC>lDWUA}!Gn0=JvAf9H)GHhV4~y}U zn>jzcj+tkg=8|9}Eh{0;^GP;Y zqP}8kSz?%W73Jfzc!GaxT-zV?By1zKvutg8Gc6ehs@K?Yho|cijNHgcIND|aZrYI9 z>Vlnmb0Op}eW_;5o^t;5z4KBn4tVa&1+w{Dv_yKdH&wvL4mKF}uS8v}spnNN_Vsn*8J_2~;1M_WFs>TU;mMUZ zDT{aEsuKgNzB_D)&R<=f4qugx%MbnE%*NItsfinHUtbv`4pbZ~mNN>5HHAaE`wzC} zTd1h3imfD7GQ&})n9v!f51nviVO0AG%62iCQH}dAmg;XzQ87{UMQV9_SJ&eYj*mTg zDq!Ltp`U;-^__>Mvre`7mtCPL*FD7J*Vi4uO#?r()Joer#`PF4U~xdOwoJ*LZ5q|M zq4x2S&=sjZsO@>T8lIgHbM*TXg7qrdmrh#7elh8;SrESmLm`jgI(ypjB=!C4%11Q{ zhg%0%09BF|T8I@3wT_B=VW$&JGE~-LXZS@<`IW{?HV@r~*cJX?Gw0TBve3#nxe`~{ z9k9aao#k~GJPk^iOhTYp;Sh{FOw^%mgpV)<SNYQrv%SDX=wAo!*hjd0U%>kTzm? z0^iAi!h_ks?CZ|dq?5t6Q|efYV~%FD&_P@Nj^`e%8B zOavrZZC)e6U=kG_x9FfZYh~&yD7Hs!=_i{^ba!8AnD5Q`m}Ea}vCOzYU;k|)H+XK_ z4Uh#}Cug;cpWCr5_{it#$f(_ru03&6|u<`=g#- z&4rVd1J|1H`AUG1@$0x;=O8^H$1Fc-H|*;t*JBXeceAK}LG|`~y(v7IyOpZGj83k9 zoBz3YT9p2^Dym!(ZIdM7i`0*wsU>nyFwj!8(ou1_1a~*lKXc?0hGdY-^?k3gnp6pt z^F4-L?7O?jN78o=sUgd?svt9xoydmSswT?198LuzvQO}#w|Sr@87wt`sK zQ#dB=0Bm~gUYvmF)uM4EA}~T`NlkD+t|yPa5_X0t>5RN+=}0TqZxGW<{L2!QTU|n% zFup0y^gC(;0Zo`J(FoTDaU$oD@v$aS65fqcnTy{8KlK==P8+8i7M^4x*>CAuZ$cX3 z@Y(tiH!;SHmM5|C3Od!TI*`6D0Ijb1d~7+;m`frC&DqQ*NnK&^Y9@Y=<60Ffj}^sU z9wK=MDdOL<=)7y--ggrV)ZmU^#P{v1pk#LtUp3 z{k&uwFX@lSP5;y%q*QSn3;`M*u|-F=rA{TOZl?9jPtdL9u8Q0lb-X8UZcPh6$^1Kt z$r{QZn7F@?dIg40VJJp!3>Rakjp5hQR3{>9$HTRAsG0}a=JdC_35C5mI}=AYiUmiq zHe5JY|8CiJN@CgkAQ5|rl!V`#E zDtx=4Qw7ou`AG5#eeO{k5EdxNMlxTL_+r#vBOEgwAVr8AF*M|)da`aR#8Ab}Z}3R9 zYR1&&A_z|VY<8Ej16fG5i}QQUkZFLf#00|2CT8FvMwl=TC5(b}1QEGAcnkI~pfp>k zD}SZT&*tP~!z7PKr{SkqZ#{+=IRdLAUPVIo{g|BBWQ&Z!&*drHh7B+_YWbq949*)s zb*$GIy<`wmTFtk@t|TI(+<})l(O-9Cgq)8=PN;HT*G}%qBMbUE1594tr&}~JZ(t{n z{rW@LOCv2Dp}TMn|4#u-9_1xoDuTCm_6F%jgfSA{4ns3^ebSPI*tzw$YZ^S$f6h zret~;ou8LhkLoe*L8a%xM6?HO;*RdKHpJm|mqWo5(} zi?WDQ2tKaU=SDm*P3+hh)6Akb<>nlv^-4#jw* zK%`%S@&MDI6YTbr=cJd;*&w;MlAK0-=(=4h9~UaZkwn>3AHMW8e9de79xX|WvO@GF zgd11kb-TG<6M^*f_{z#y95WH6d=-VaQl-9W>LX#K#_;}< zWVNF6Qos2Y4D4k1@ho)==e$iOeC+AJpJzG6%JZ|dDL^WpHrxkK7648x)cJj0sOyx<+)fyzlYGsn>gCFeJ*o_jgfJA!Gto?JZ9aT{E%YuEEm zq`jYhPg|eF&^s>k1cw7gZ;7iboOIW2mcle%HJF^Z4H*wHfEg4+N9^m2^9q1gV| z_%JrU(&Fau;8kB{DjDeuWpt)UQ$%SFodlh)LFzK(8ED7?dNY4rmGLX3QyTd$;q-1M z4GPdcnNMYRaPpTYW{Kbt=grxV9Svwe-m_}ZJP zx*jSut2%s+64QgUQE09isW{h?S$p-8T+M|=56j@{Iet}_2h1X`>a%=-iQ=4ShTT@E z<7^Gx_vDxl%g|t$O6EzLpt5fR0AD7<&|@T}u=L4mu#}HCz96{}0zvqJh0G(RSQ3H^ zhf0(XWghr2HWgsC)TIXJc}qn}^-41S@-mB+tzkJA;}3cu!^`?Dr!U_w$kqlndXTA= zt~dgj$GK7K^Zs?QHQK0n#ogF>s{oDBDo}b=@(|Hh|6w*{QlU=&l0na3ve)h#~MEt7>IgqiTr8F=IpEz)d9#*1iAR{Xu0Q? zm<)|{)g*X5H!+9al$V6VM%0!c2KUe6LYVge*+j>wh7Bd)$&vXb5 zlK*8Q-Pqb#R5~R^^}{jWB3(o#ITL*}X85+&VrJdTJ^>W-g81?yKBlYjEF)-nSKFXnwV(R+#7!Iq}cmM}-z1RdBl9jh7AlAfh%kS0lMAGc(Q7Z;KXy{ro$j&T-I z#%I*SOkYU_De};j=tArAHIT4GfM%yv3I&OTQ_7!_V>^6qZSgwJhM1K-Tq7?ar4lPRSbC@o&bRFvF%UGEf=meb3;@$*91n{LCLwy3 z%oBajxzbR~`W}Jthp&K6)5bJ)EH_c^0epO5$|ngv3N}Cx(9j^AjpifGzc~fto`8Pt z9p*PwH?2pbfe*T~5-r>}|KTL(zjuJfPknu^lpF@3=M^OYuxs%I7- z5d#B>tBT*?ME4^7g(IvAy_DR!>q+}d(G)PnuUG_&Cz+dJx~2oydSoIirf_rGh#z~| zTN%DNO*Q0qEC}{!%B^NsQ*Sc!2em9G)lg_j^T)r0sW}z5urpV=_Jv@u9}DtVeIc=DsQ$v>!_?%(4R^Z*6fvF-rZ0|JY@9Gd zs`)!e?LO@jQLYEgw_!vA_&k4AnNkEk>=8A0GomG|+d3N+P!xhBX8cQ=rrqS~>on+W z8@2JlvNrXPo&n0#1~a638E+o-?PjAoyfRVx$PAifjW})XHPLAc&Td<%?y;IiI1kV4 z;0sQFVNIUwYR3@|%)RA)vsa>%YqNwwbip+O2m35OM?DyHwB95Gc}w(k(%)PQNhFMe>b-##yC!GW&}tXbQ(4W_N4-ufSe}&&}IH&aHU+CG$2T(o1dU zL>zNVHI8w|o7ZS!!(M-R-Y#;AjxP4%oV+i#xZ`eqLdAMX1kzlrl8tD$awqq4GDSDQ zp2$OLDLs!9m}W>(1X!~Z(I>FZa(T{qv}Y3#s6GPB8r*$j460>d7nalx#pf0V#mQO) z2P(u^6*L}3WLP}_!Sf#@>@b8Q5ei2~HEHwBH4+k&C@!TkMZ!LIl8W)?M&z}H)1IRw z`oBuIT}{x?dEkK3B22m0OwTG{>{C{n>;;!Qdbx{fhD8^s1pBMtN-KYRLnwSALjzvS zB+pROxmEbPEz8G+0qNCzf~pDW9-aRVsd#5(XA~-kyJpv6m=PcdLh$C-QYBC81pcHT z;PCbTQvWt3G#5ob5ds)@mk)t3)AsFJ*k8AV%(nL1H%7>cl zXY#hDAJv%Aw5x+ns4hhyaW{5y0cLX41^pwKx;SUH%QCyI=y3a&W@YT*A7$#f{7(9- z!eCusq#Q(ZL|B6l_X+ciq1xh!qb;$=?n+uN7(-Sl2d6gmm^9E3|Ap)|JE&UhrGHrr@b>ij?>@~&Q73BKbT z*OF9mKjuQE3x(o2NJxw}W2Y$GmB*3$Xx_p___k*X;Dq<4o*$uG0?0i1Bnj8nVnXB7 zbth9vHdNQAHqhLp)2z6B`K3urcYQGbc~o!*xYKf3hBLA<(;q-(aDkep18%iDib>1ev!nfscM{%lL6q!&o-dy0r)e57B6WITpD zuzrIm2PQB^eCbEies2~6g?>?rPu40Dp*qPKr-8RiimWoK(-tCsM+c;?SNWJvWrWyy zFPa0pJVx@FvD)kr6QvYwV7{bto~@C0ZoPn}?bKQWLn|nWwx!J3q;;f5=z9Z?Q7(;K z*_@>*d;)5PQM`sq3QEC!AVINzaBAy4Xu+}gD=#92!RUuN_kB4{zLf>gntM^ye{);S z>PqCxhmwJ@$M^nT zQK|{!$%u09ZK^sB>xbDq%KR0U;qlC2FYO3~o`6Bh;|Nh+X*^mTgol@O@`qvUGS%J= ztl4hjb}g;RlofG?Ts}4Fdjp#5el85`$?$=~Y^w&qQ*k=|dX#&9xUPD6;EnjeE){yC zP}V6@k`I4QO(|?%RIxUiii}ufF`6Re&Ta>Y$kns$SR^dLj{+6)m6r8zzP<(*Pn#IMnq2!cS<2f_%mXR~^R0G8GmV}1Rs~tsTRv;X{tBeA7Q1c)85p?JY zxkm}S$n9WS@&M>`p29-Gz#Ss52SjhBV6*y!klxmTu5*CGmlo(q4NU+BX{bGPE#7j1 zpNd#`Om1M&MOF&!*x(EsR~6?`doXfX@G3ZPKBl74@-}Js2v;_Z7kwrGf8tvNples$mM(4QWzB z5}CsUEHZTErc@%p|7d1YEwAKxY-Uq9buL$|hL=hHXi2f_`=L5x98=^rsoO^sT+Sxc zX+;N6>_B6eR4KT&6KP4z3RbFC=&%oPn5xoR_k!8;Uv+14G(TEZSZ52Vj>nhEwgzCm zYV=NgF7!|(_b0YENHzj31~@EOjAGMAo%MBu*qP{*(rF{wiH>W9RP?5a@AR>EywG|3 zB7ZrvtE51U%FB+53ZKu)cT+f!wMRZLC4V`N=Tq)qkg4?Az27`dHZ*4Hl2`{_w(?5jK@&uKD%|xoJZ9J80_}-8ws7_@~U$ynRNf!Pxm|$0gr~1Z74&kD2GQ3Z=<} z_AQ=Yr;=$Koi}#{U1fS`JGy?Atc7(BSbe$Qg5|jfH=6~Nan%UO>Md~o`~-C2^AnGY zGnWdhIdmWiUGp1Zsd1NmXgcIFF)H01kxb@@R=Jt@3v~CK#F%h~<_bb7zE3`ES*xZu z1;;XbuKZGNf$*Q?O|aGj_P=+26?aG0{DJ30SA=NeFS)q{xwu#^RrPu3cUxW)5v_G7 zVc*3soTMA)>K{BUKXQM&T20(1j8cQ9XitaS((0Few;Vg1O`d(JMgQU`-XDlJiKgT) zduQaX8b4VRx)Zh?AcgnC2m90}n$b9QcZk!|qtm@$nj~Fcma$7(WWwP@2+zUnE1FfFp5TVkp408w_t3fs!UUGQ8AmGeGV~N$=PW!0 zC(Wzn;W2`r2+HYre~U{RU5H=~m^?>=;Jn~8(MsfoH&+-qFQ<3PL?B&1fJfbg>k02` ztXzRP59^yQV9q)5KPsMpmx6#p$Y**?6*zV$^LK%sF0cjHfR+buKXW##1y9+4lWvAI z-DXcpYxrvAJ|bC6_u_DP2cm0B=68A>aa_SF<(zBgtSVL z|1m<#Zo)}DkxV`KTHQ1pAzoYV7G*&ERXC6c98bN#2Fxrt*b6xjB5&4AK(C#(rT+@e z_3hdHy^#3NRPs~>#gDa;v`m3bA<@v(B&27QMGEILJ|$QO*dHY@y1G^TmwwJ$+C{KP z1!RJ6MxgpB@6&um??aJ%6?fEP@hyJzotl5%>-e}F0m9v?BIOuw*YOE*D&IY^P^%PlJI;Q}Ley-}kN2W#;JUxsu z{2TKul{If7`^-G*jNCC_hmKP=FG9`Z1%wkPdVR)nm<1n-%5;auO9z3&*Y6- z6^gye6sfH1a|;qgi+8VQJUb3Y|B7j4u$WM$ed054-Roj5`+{sy6wO6T z7nGg`?1eq967m^a^hoI?L)^)&{tf3xJWj^2DS?JPxe*<}g1`PT^+?*{>xAxjfxfM2 zoi4^!3<{5LJRqCYgwI9&1G>oeGikS_B8&QpOXc7c$#7s^fUMy;LGJr+-Zd`;7lz*I^2Zws zCq)p#>7K-=r9hhcrae);YNh--W`hk^JWuGxb(P!3OEyW=3Jooa26c>e z(L@SZ^TOQ0MWTqxE6O*6Wg0bg!q@8J8>veFJb^KABn zzy#Ee_9G8L!7-uwn%CS46Vfiro|9QuDPB>!9owTSDVoyI%WbX8N zZ3xkJYJ7Hg9J>Mz>}2Q;7=zK|ccg1q(qy~6GM%yE*E=dQmjJmNM9f<`W_C|VFp~Fu zu@IA>oH>tD$aSAAEhnEW2=T1M3wnD^vz)4y%&B0vl>AWc+fS0gi>l5ok?VR5st*rX@Z(PZ{YR2+^=Q2#tJTvOAaT#o6-~bA%9$zgqh& z{(hCb=ZpyFh4hPd7fFawvF_qto5>3LMRV!BGBft3`=d%ubP8u8_7c;zDQal`@zKfU z_OP1Hm?5~a??0R!Dtd?Z)Tv^pvcWc*zsOpV=5m=w63&6brEF$Z5c}G(W`OR!buM!? zL)1{CXXxWqoHDLfb^JKUHF{Zf=Xi9FiptCFitj5IExfCGGOMbd1C?4Jk1J0`a%7nz zq1i${*6MznWc-ARodUbwbE@mr@QvY(dV*|_deEkGP(Q@I5{#fv|5Oj9Reew!p|0ea z&xj_@xos~uK7N%mB2=L<;Gxgus0bb(bDy-SP)a~gy*3p9d^uf}c`j1>20C{%azpmH zG)IzG!D+Hk+28RQ!%_jUAR#uW%znS!)-O*AAmuY#{MLK=8Gp_;EcY5icw(S1T#Frh z&z)s$Obvz`6&6H>WH96rw~nqY!c_6#yPe)c>j zAEBXsNew<{dGG&GCCTkXKnE(qm4J7TtAb~iwz%5fI%}xya{xKY`K{Nxqi+7b1A8_n zFItWZ$tvZ3M=FF(OS4oVyao9q`F_tD%t(6S{YvD$R%Mh)h5};JYd4z zAmboHs-tfbX7jj7IC!$aUTy6q!NL=Bssv{fUjBL|PUK8@Luy%3uL!^4F-l}dBb!4= zUF#zy@VvgdX~xdvC6S?Fp&`k%Dt$!O?=z<43wyJ%x?uq}KDedj@zX59b%-g&=<6n! zDfFVCjF($Y$B=1!hZ<4Jgp`gqH}s&jSe0N~h@UUUl*8uIlD>KA2=)=0t zoNMjSRUy%=FiA4C2=&_}L#=h!iJAS&E|swNqkaJ+ds|6Y{(br!er+Q;2quqS98Jq{ zhQEE1<&6c_IKmKz?R?r*d{tjZx6N;A7IpXB$lDb!gFx=$`}ub@5k_ z;syLfj0$Bo2#d*7Q>2goXrUW|5Bs`k4O4=~oXfiN<6&y3T4AH)_1SC*=bJzx9d6p+ zp{5+m?>J>F-`qGGuB#G{TQt}@xR?-UuEC8cD3<;hpI=RD5}MM_25PHt#J-SwmCZE{ zo(yqX(FIu2C;DHu?2ZY@ea=I;EM^3GRUbK{Ted99Nr+O)s0=Bltr8>W>nMIgO59WuRDZY$ zJviZcoN*+;rTf~Qg=ja=b$6vc6NGTWEGg;9%SKD+PSoV`n1;9vYL^K?KUY3F;xf1# zX1Kk9k9HV7GzbE3hr6)}VlhXuw*gsIC#7D#Qp%!Vo{8o+6*{>(pMlI$TZIEeY0OSe z^k$;ybuj`{I~r-?9d}n7otXxQm9%QU?d1TOv;BkPx3y^+jyT~Q>%Sz+GePwCIiDRU zqO7xCb;68CL;gFs(1Ki`dz;kmXqHIjFu0OMO$ zvCoi=EpV7qcEo$iS}yt37A|gCgn~X+5N~+MuX|MT3y0W)H)*MfmQ42Il)E9+7BzTr zJ`)@QGJ<&bAurE9lLgzW&^vzkIMe^D_5EBC7!nn!6=jx70?-;w@iMOSaFGkn!Bb?N zf7%De(?N|^@ft0vlYlz{8=C8xYBS@2-NMjA{3tJ)Kj{4*VNRVxuZ&)_`6W5hbOwRI z7;ilEi($pd9krSqy<-wc5t(r?;#FSNH*$)EWW-T|JJEfvyNm5p-c4yCCj4{A2$Bxu zvh`ya=c3W;mY`H>!^A^~i2}~oWpvG@JGB1NpdT+eOEV24p+IWTy=QI9^TC<&3LVAP zL!I-|`d4cFqSxPcqcti)-8#;icJ0T_R|m&xXTna;yn%Y5dLo=ERcNx`1Sap56eJep z72-VI<7~{Xb1t1e8;gVpxQz}oWhrgB)ik40NPA|4fk70IJ*H{=&fga4e8Fq7jQuLRi-Up@x(BHTyB>24jEjSbRa;}eHY}~dTe$2ZNzstZ8SiF5` zVeqdWPm)9}kH?OV&N|eC;fr>iM51?X8D02`$$pwD&81OU&d5JMBDaINxE+O_N4)W= z=rs843^OHVHUxtwvW>h93+Z1X$*kJUsyRkbCQtj81=zT&8mplnfUP)jA13#`zUm2B zkeSjXBE+F1-O=;at?BN@uGi1>O3o%%;b8`k@Wvq6jDgUJK6lFlkKJJhDEZKQSjkUy zSqXq}E}z)K1NnPOD;%6|*ihbnt#+wGaZa}pqU4H@(kpYQz(~#)`MGSiCQ!G_odm9s zHd>vH5Hh3voJ4@Ak7`Sd{9XAFvPyI*c~!HoT85ZvBJU%ZpV9+&0p{t^voM(bU=Z+! zT@fgD($#*4+bGwV02i}@(vu5`kZP3#@(Kh;-X``n^uvXNc4b4iS7T29EOBxYa=!_H zkE@YsPdXG*O@3rD=X>p?E6A<9FYs>p@+HR*LtSLq3Xp#g6xZ`4MNR1g&+LwnX0I4( zmc0Lk$dUj8^W70mXM@cxFPG9ex3|bt!-nN-7pT8J!AFCXI*P8Wtj5(|bemy&M5{`> zr;-L_Uh-byI9gPg(D<&a0YbtR^!p`Fk<*e+RoI$Bk~Sg+FOaT*AbWWFp*P@{^vRoD ziG@+3_kSE}UK(u0lm?8Mn5PJUx!+)^=$rMla`wejfkAHPRR=JY0Ao4WhfOtl_+s89 zkrcft&H;4(H5?IX$oPo-pFS!je(Sl;eJDJUvA{1im;bVo2I;0Wc)i}w4$gGuNNZ-+ zX>w{FerZV1(@Tzs?0fcY)on#P}$ zG~m3CcdRs;h)Vxa7&_;KsyjtAyh6m@sbEO5!Zs@aN)qSive@8KNuvY}8XYtAZ{F;e zP#Pb2Cfj}%!ZNoR%MCtrymO&U8ETH*bu&65Tm?a%uvRM1xy0A&lZpGN%+&lr z;v0(6-Lkp;MQH8J3-tJ7xr7lW=suVXJ-_3t`1ZhaW6Ga(Eop0rb@ag4EvqHAX!shr zhX(f4kj4i3Cg0GQxTw#?!rL`QSE}FoI4N`g41;6ipn3C4bI->NV_A%NiSM!;{dFMT zs-S?XHgC^8^DNBjQn=Ts*CT0g7ys6V7aQYRQJ@Kj_^b0Dr7PDphLy1eTP-?hRtY82 zsCE7@O@JQ8lY77Jkvgm92%K@ul`z)v2eV4*eRSXA;BH{sfv1j786}iQ$<0hk!jgB( zx;fWrxus*G2~;FyRI%gr%2IGy$sD;W_Icqo+| zl+$!VAL5z;wIHGVxlYYsk^ARwz=IR2Hp%dB$}f$s$H;fci?7m~vt8BYzX^~)P=m7F zB4N6+=G}Xe;if5JIZi(_G}lO-C5IRo6Eq8t8py!|N#{jt}7?&18~clKtv zJ$z4Y{d4UTZ1M0&#V~bN=cz`I@A}S0#BuZQfZs1u9{k6Lgy(GQ|2FhD!XFMcp1l8|ObBTBCZS-CJcb^{Yn~m!c=P6bXAToLn$h+9<9=7}N z|8Ras+`R}lK1GBcgoPe{qucOR|2=V=eerRf%`Y;n|EK=>;pdlIq1!HkPbz+|Y!p?V z_Z#?>y#s5KHKdKV12**l_%FaCt?rh#6AFwl+X8y%CA|ZR#`MK{aQ=4{OZ+8#HWHL$HAMu zV+<_jQcyO;ZDIuNK>L%63GR9Jhl=%chMJyNA$WN#Il)<<^~M~zRvZs{kP4xsh&_ai z0yW!geX0`570tgRHOc4gY`{H&SlPq3ubx(H;hpMtLX)oU<)m&KDei@sK>R9JRNQV| za5`-c&rK-;X%`Yf{SUsz!mB!Z37FK8l4@HZiK0kI$(C6rLyohmo^%7rdbtsge4FH| zUZ!6ejVVJK{i1Z(sChBDVlJddd2 z2;(hVr;TL`KU8{^5r=7Ux&S0W=RStm#SP2ceY|#2^Gpq5R#O&>%y5B%V}Wamn|_YR zttl4-h`MF$D5Z>^xR_Y*yiiCjbrfz1^ zdW&EH*$swUCE)~MS)ly#>X=GVYgUW}i^zA&Dp#k;)O~7%v@Uf2I!!N1cl{sE_zRuN zf)ElXbJ;Gzaqx28yFvzk62feAgn%34sebopvoFzdkD1rtc=?PgYyXz~qw9*5ci}(k z%3r>gLa7ta&c97o9VJ%A+KRF(;= zg&Ue{fj{V_YKNd(;_*2oj+QdJiqc?kg{4TRt*-LbVSsycU>2?}Ql3YbOKu#2SnaP5kup+67DIoKE%x(1qZ#W>FGl5eLsatJxfDV%V}CN*t}L3S z|LTGU8D-LV1Lu~s?C|pV#0lP}R<)6I~(FPEf5|a3VQE!$zgG0tnZ~ zPIP8nK+;rZ8I7O#$mFsfImVSPlk}=d+Hz2T-pj?(A2{i9^ass!E zCNxqJp`t8{e>c_>)oMW}XXqsgDuryKvJUXiA89a0D*T!i`mWYjnW=JyYy5pR37+d@ zIxaWz-m22e$zk2pB<}794#X=_A??VoJTKJ6o1Radf={*g=jUJ_MvPGZYUyJ>u?E6K zY3%pfJbKRg=a^b99t-rC_(a;L3zns6^oOS5A*sv=F>{%39Y5?hAR)Pz3nmFAsOOoj zUq7UCaqUYz=@#jB5;B2qKl^sUFQRL?P(rG`x*TLdTfs>QgcP`w&h?mb)QasseK6}J zyt!t#AxnblsT?_H%6OH(XGvJ1>aJHD z!$`clCXJvXF;}C>pYCm?7bLRiV%1idPk;8Fq2~mE&7NHwijFpSRKgT{%DGLI*uE{4 zHx#D-xDjbA!{^xVN2t4^1^`;b%X(VVfIrA>(mJw@o9 zriV?g3DT(muS|Mc-2*sc>MozuY2nW7q=B*eP`To-No1G%-a*#dsP4wSOsm_FWY0eo zB$YDb;sk#n0|05ULzp}FT{Ja82a2j6@5c5{re>03cOo5!;7V%B zg|KY?Mdd%x+J7T|o2{rTbx%b~2up#3jCE^I92w)VFGdg!~;kXI% zWgP%SZGh9RdwGFDR{=&k7r)QxM^WDL#sQxsco2T!%IlO|AQDDLekiUWAvfW{=khrg zeHGx*||-|bGhJeq}ke2Sp6)ulvfK5x`hM`Q%dg{1Yi`A-`PWTK2XC{ z@r_=uS>3Z6F{U&*^T`Sw#AY)zKmn+s02%&uA=#E={inMWC%7=X15dt=sWgm*8_*!l z^jBeDFlOT6p~&wDN2?-XM9r0Tj9W{NTES@vMF?&^ervBqHzHFw=?qQOrz6OY)!>>k zfPS@~6j&IM$=A{?Pt4;*nrw?0x^ln0&IhcY?Rcrva6qD|XzV;&Hfx!h*%vTLES~KO zt6x!VyRtOTfnZXhJVQQYjbd4B82_i$n{?}%a>29H4&K;l+xtpJiVowJICQZ* zLboq_=|q7fkyk0yI_4Ty@GRD`KXFk@#o@INgiWP<)1JQ|S%uh7lGl?wiMr~s+|CaT ztts4%Yom*HOnru-+mu>a-7O&dgL_#iP>Pla3bydq;&e1vmlE}2;83cr{pjg&;JI^P z4L1FGVJlX|Yw5RFke0&B**HiYl&OB#Ww5>HKz%|*-27*n-@dEf2KE6;fq19?w-wuP z(BH9@OR{Ezpd-a1)&!v!b?l?e<`N9Ix9pd7x(`>>xdN|x2RCcn_-M^OP+Yhk&ywEy5aSsq=EFt8Pj5sz9pbOaPFs+WudBzX_zi zcE~4vyz((>HLHrGwicl5b37m@BXHgCm%=J*?_zds4}P`%-b}^5wxZ4Lnu|HwZC&sq zPb5gR>eLSW4xf6C9xbD0BfUU0A?q7iDVYQgoTWW@S)?U*Iq>h3dk1xja4zMyEdjVm zY#Tv^E48_c$uzYO_Rf5G>DqLhDM?%1xp}wySNypX?6kL59h0EzEk3dNsh>9^Qnb|n zdYV6FOmvLQ^k~S~^+~Nf`vqFonGEZg{C&x>fYd%sYugqDdP?P*P8=m4BBER|f}PBo zVq{D{=`2r`Jr-?_B>PfV`#V{Bl9$bSXjV`0@$1^xYhD4{pL)DMEr`1o1SF-@qQanl zlq^*pch|P_?S%=$z4#>N5@kBTCODe8O* zc*|U;zn>_;l14Ec9$R;YN5Usa6k-x9*>8wd{Yjah`{-U(-pp z(ZbXJsZypZ0@UWpXth_ri;gJ=Ehz<*aX!;$ZX^6bVw+~{WURN2oy5~z%d(Z3cblDXni6lF4b-Psf0L}T2Zsg{O!35Jy9771tgW4Dh>G_}Au{i*0T?we z4lk#nWYJzduM45FojN zo6qv5(o61->w&`qk0@9-z+|<0>a?$o>+Vit^CH*XQqrafnA2bofPlI8Hr-$5%ISa! zZGeyOjNXsV*XTH~;F?5dR)(`uS~SY8VB4Hm9!0+*C5tz&vv1?AKpk*eHtiA1+MDrg zjp2#A4_r42znX6C6*j>6CxgU_lqU997R-2)rSgZYid=HCvuS`RJBamB>B1b?Ko1ScPl1HQ@0A-CK*IPDJurRIdV1L}Hh;%e;Zf6ptk*ruY z8r7+O0J6##)H>FW*EA?qX*g8+M6k|{9uGmeB!@ndDZ~Al1WD+dI@f+RQ5z&^yDjV1 zGjI@wigR3~y6V-o!GS!$gWv}cLe;lXg+^|~C)k!tJxa?vP_|Iq_rwUpI2)eThti#6BX`%%FrTJduT2{PE3X}IF zl5}|`hHzzydS`H(V8Bdh;1=xggFbxYJ_$Oz`6RS9lUXt7I!%D0FZ8ZX#)%GiHC9RC z27{_hL+je?7K~y_N=WA15N8#0aWnU9#|sJa+#l!X)H53_V1LhjsS-r9?oe-(W1n=f z?wssNq?0CBxt$YHZ7g~H`0;7NvtTi(e8in!O)pJRhSUPo^O-k^AX>#O@#%^CGMY4P zO%a?43Ol@c!t69IFCj=DwO)}?Pq8X>&!OOSMqdyn2DqM9E148hlXo9^oEJo3KE5^O z-bRJk)-4sEwM*EH74$0q$ruZNw6T6cZNz`5C?)Y`Ls7A2n4$WY@~FEZ%~e7y#nS@7 z9H-g#x#oX3d7tX?Udy>|Qij6Ob~Y$A5&AHwxQr!=rDSCOQz~5Lz^>vb&@NRWqg17OH9tZ%PMNDj!PjK0Oew#YP-Bq7TnqjJQ**A|jr? zt}WP73xU+XQ&kFuxCU~l4I7dF8IT7>mb0qHzotLv7rqp#CVcE5ITN>g3q2VGlX=wr zGEIlpJmTA{EB_2kdTIl}~$=@fOZo zyQ-0$e*LEqc^C0vttG?X>7R*-C>u1!ZW5Q-`Q})r^9Ah6&xOd<{0bYQ5FkO$ZuW!2 zh?#sU)dT^`5G?P$*yi7_$snYGctp2@plxJd@o+U`@}J+_E7ZY>k>h=t`Me}5L?{{5 zvxgF95ruz+iGD#JB5zAq!lbkQ;p?T;M^FfJ> zwwhPUn&@@H(JkC8S-wz}Wh65GYnQrwX#W>1q$0lg;$0;lQjY98T}^~snBnp@KJ_i% z_-hLKtD*`nm|E-f*XD!_-E7go!lAYW!2U?J&X=@&R_0XR%{WDiitvN4LmN+G#yfCZ zLNczsC-x+P!egI3Mijb0f~u0Fxo!bG6q?3=X+Ds|l0aN0C9noqoA98NCzFBq>MDITV2LY5gn3_j} z(Ke>a7&O|;E>2R?^>HmUR{kY+f` zZ!Rq1z69FTA7vXZcmBDWzu8#rSXKqqWbJa&sxDtXS1s{%xKgSsq`}fFiyjT3flK6! zGny1KS@I+eeCWZiq=`y!Yx)_9>H;jt^9Wvkvo5B;^PnD9)BNMe?U-yFY7=LEA>5}R z!rS1#y0C6ZgiqtaF?e!Eb@d;PT?_M#IeYv>Ex@_Y7=kL^#oQ+pC)Dpb`Ry@C{Ei{f z%)Jog3|A@{%kisK*q3ek)>XK_LZBjC6Xa^@0{_gA^(xX?hI*)!?<(`n;)~XX<}!o-A>#tlkddLnM1&=h1L?o#rJnhrcNtqf?U6EgeA@K*x@yG zQNndxMeX%&_4Wq%3~9joaEVu0gUJ_am2hFF0{>u5k3Sc2<<|!ZjdA*K1C#I#=>GFq!V3pnzjgbLH14@f!6P8f2m9aU9~7tA1V^1m zE7#snLC`@l65@rRr#W-YgKB8r6 z3t95{SQB2?K5xTuj>3!=!JDKRycoQuq~ls(&YK)jlLXh2Cmw$OxXd1l2L#4M-pYWvD~YjF23h z=I7bB*Gmi%nPjI)mIZ_5vLC@7t5YdlP?wPAwjQ>wg5ay7va~HOsxJM{_h^|KZM3=u z{Vr0V5p1lW7b@7=Rxw%Vl!OPMdi=sL+P-;MQGSq_-++jag0fOz^A^70PwO6R+$tA$ zXE6D_eJ<7Uqyh%alt3VcYd;izcC~l zczp2}A(P~1+(jw8X>Pa!BReu{1Fij>pP3vKl3MXnnm+j)18bLusHTUDxmoeQqOo2o z(J8fWHje2HPLJ`MSxDX_3So=?c2C8WDZaOa!GB#dA9$Mg$Rl_BT-!ZunG_qrPDs`T#%a8@(Hpk!JyCuOHGCSe za8|Jrl|Q~?X1?uKobVuh1#9s075gvbP)wk=TLfQlXNa1JDyvg`Vwa(j7Cm8k%In$a ztb>=dYLwXKG?64?Kj=LmmrZ4``kDd5D}kW&`oQokv@2mY^&~g>0Nik{Cs`L${50S1 zu=YjLuhqO}L$kFW(s#_f)Z}$*7dz(muTmGiW2OJYS@PwO%PeQp{UWa8Kh6Y;gDjc*v_upV!nw=#3^p&$L& z-hz1xUUSKm6#j@s9e8T2PElMjDE|x%`&|(e6@8GFWBzF zSrLP%d}PyA*lsIJDnev=BpuXtv*S2`sQL$&_HEH5&P~oY{IHHtFL-zJ+Oj({lK#G^6?tKhr zkA^>#$6sws_}n|KULGMb;q{R~V2DcU`rWq(1y;iYaO$M}WP>5xrd&NHL&mf0c3F9C zaW&8eLZqm&bW|6K3(pfvdxkg~DTqsq0KaiBZj@STFD+4NDd?!p86)N1$`J6Z%_i|3 zeIGozaNsz6+7WzCK8a+==2<1>(E{fEV8q1`PZ4Snki~L|Z3fwcC`taQK6_OeAmJ39 z`Ytp18DXMa8;OE1fZ@~pc;-KgXbJkS4Uhzm02-vWn1EZpM{q0VjdlO~AfXs}V_2Co zHBvTs>fTf3lPQQAiTgL9?9Y>-pq_^nCk;nEbC~-lk6iMkNH~ii*WWJP2%&mSxxK#T z6Kb%;$>+{;n;&hU52bA1Bl(n-6RIBhsR$q?1aQz`l5Z`BizWW@y(=d8xJ2bDglsAn zk*_87HQwbCDlhLfNl}rbipn#VMqF@-@VJdzEuFR8646=RWFfCZhXVD*BFYlXGWDtCHP3&9Kb?)m{~}cI{jCrF4H3RJLK*kM4Ynl0skz=j!0_g5XGqGGOWu27w^j&i?BVHXDbSQ2h-6v> z8Pw`H@=i-YY2&^lecq(lSM-zWCcuFaZlgxqnx#ax6&_|i`w^R>MBW>(UIZ~Ia2Rpx zvt^5de!+i~0t>HkbVH242~8wdp9=*EEk{CrC)tmxb6;noj432tEmqv375P+KUx@k> zW3wG<7M@AV=HGInwa4J?n^8c`s(gIz6!P>%M3;e#bz`duUtVyO$a?Ft;Pqwh=RF5w zF=t{b@uxXc>BEDmNU8v9Pa9js4vgFHpjh(lioEdGEdY-n?X1w=hbvA(-A7zmHpcwk zs*;6sFmx1@9UxC2`F3>Ck8E(b(Kg$JA)(FLNcy{u%;cy!uhwJ+y^REV!Bh&v*O{Df zKEkNMm5cYaT+0KQFei82t+WgW|Lux0(8>5iz~`zcce53}iR~Ht5gToY^A<^sJ`TlUU_&H3YCX8UE0X8x6&fBz zc5^tDaLbxy?fhzzH$oA*JUPM;RNg8j_qtu~01me0`GKCTlw;1!ZR|N#@@n!+<&#%? zO*L1K0uEellK6IwFby7-yJaNwJPx>-!qZ=%I+12dD2M8sWE88IU*cw%^I{tmde*%L zNpuiFzOgP;TU?yd7UL6?J_n2Wg=_Scs0HO9WQxk_2JWPiCdPaGmZ1X~FM5!D281#U zxx)@j^||>ZNt1mFPIQZMhr*e4QdR0b`aCB5B;!=($B!~j_jp~N=8Qihl3G*;9Gkhw z4=rhk9!6U6em?xF>MSEVj{|1#V)SQ+&VbcPX=@1Yy4V}p`72Dv3c4)PKD$gUjK3xK zghX&6@%8D4%_EE;-aher#mcE%p#A`7xeM2c#M(_D&3V5Z=5sAe6ve)VnxlX0L+658=BjHn%*j0*76^nwG*>^~W2%2eseH5` zZOg^=SN=f#&eu<#Qf;7Y?HA_Xj3?Hw}_XEz0pzUZ2pg6W|1TNZ$LtC2ZiBSfb!H4M7&@GmXJJ4{6 z@#KGeomE&=Z`k!wK_sNRBnKFBfT2sed+6>?X+fn27;@-thVB-X8fFMZ2Bf8>5l}+n z|M8xE2k-SB?~}dvwc~l#z1DBBe3vQe_1Sa|74yKZ#ivrwRNNWF&G{~su6qnYSS;lN zcG02a9Qk37vDZ~@bvA>KWGZG1|6%DJ;XlS`XXc_1kcP}`KL-hlfjkzxk}B_ALpVT; zvX4w;N)!@kB;oT{a0`haO!i~-tr*DFQagT|P%8DL9M=I@k@M6GY3ey`D!n*kQrX`V z46=FlTbqIF%$e$A@6_zn@wo$sil(-Y2PtY>f!L!61hKZ9Qbkmxig01pLhnCtowmQ% z)(yGu{vgFSJO6Gqk&`oYqh6DaRZ10anLQdatr9W}-08q={IEFidiU~xv*vitw*tp6 zi=*Gj3=`r;+i^T}z63Qhag8^ePN8s%il;kYnsev8OiEB*r$l}kjvUz$UDXbh~IK6Do}lTDba}l;u(ZoVSvJw z`xLfQQu^z$Ic;bS_A!y6+O#e07Nq^`nJ%_Z^-d^2F5k>aM>uTMcDnaGZ{fMoKL2bA z9xeSqv%(th!u_Z-0a#zoO~&MGbD*t9NDPaw*F+_Nk5+=Epe?bhTs3IbVV(!G57O!f zfN#(|B`2xvz+yQc_=sK2Jn~wJHgn-5kmUW+h*p_U-4Yogdnl`J*D$H3Zyzb^uhY~B zOyhYf`s0Gq^bYMVU6MHKIhv%6RM~cxvWm97vxN%%4CmaDryf{mKCf1blDzyq)0dGX zS3=|u(=gy<$IN>>brNekvp6+44D`Bt(w0Oo`CYaK{W7qc?{qK`IbqBAn68p5Mru&P z&?iP>{KMHii08?!OAT$X3&e>nbA?u1^YYw=J-4R>H2&s8?^Zv9YVlXcMhfq~@m&T8N?a zBp3GWEh!2e8hgl6l=w&!&4)(76EQpJ%w-coE8W?0J6KiVzqY7l+ zXj~NQb@G)AeO0%7Se1;xB>EF}8}7dCSArKWJ%#ri8P!eIC4TvE6|mBnb-I;%aSj?#(G7dCLEmL@LaZNgTL)W=^?BE!jh~_>+!|pp?6~pi_t$yr>*s`8rXUjYm71W~q1;gb%aJnXxKES=TQ;e36_k#} z8QzqW{q)c%0d!A3!kb@E81blQOo-)6WBFDFd1p4vD}l{j%)a3`irgjj2i+6yfJgoL2h2aw9n7QIJSOkxS>L#S&pxVB(2C>Z zF3>i%^u6%k_|g0|eKYZao%{Hw!NhpQ%I9(j;bqyu_2!yr2Lql)ybD653>rKm<1W@+a0slQoPV=k0yp};R zTqhE2#f`l%tv{cwgwIW`5~y=}s(P1AiVlK%0962YN@env77+&Lz{q9bbXH!>QbcuV z#^2Oz*4WF1$F6YFF^Y5dj9uy2`jn;{bu;(5>qb|~!6rB1{2L*vD*o>cZrV$Q2a7Ei z=6>`fu6BD2*=9k4@DU0rilZDy51@L#W7|s(!@idZ1fy~|P-DF0Os6Em@VH9N9Z?SD zbbY@36~njc9j9Cik-3jCkz^T(D*^4o*&LHs?nDS+>F^2hs)P_8bM_W-884yel_Z(9z?DQDk`+5b@im+ z0o|n2aH)wfVI@d$pCMtCdB9zkMG=mt(O6JAyi%w}irMC{ZPu1|p;$#F@Z^kp=BJ@K z(qR*_^TQ?<;3oREr{Huzm~Q;s-m==^VjOT|T+3btBAsqe98fNNxkqaLzOG$ZYlKVT zOn+3{PYTRmlz-bh%8H=!Y|^%fo$ z&uhj+O=u>i?IcUp3|r=;shE)s_)}>!!HL!xJZIhtSXCy=Sl^Hui~<_g zUJ?UniBVU`X)Jt}5NAJ>yd6;M9!x1=G>ooV?v6xXRLOq=6ZAScnPC^a;gzot;MaJ~ zmO`1(ZAF@{OsVi5?<6PSg~;qn1K1Gjdb<0#=M@GR!4!n0ZOAfQ#I3TxNYs*HvPe*BwC?D zCjoDKj=bl|ADpQeBpU4DR zs!``{lmWYf^3jM;U%kH$-WT&d-@g0jZ{yiFMlyuuLr%o!_C}R$)t$rdt$8da+&(1t z`0W(>&?yogPzlpnz-n;|FK@4Usy?+QH5ep60|UFZ8y!!gd`rC>P)DP>b{YF{7EQ4X z(7B4^M254_*5k^JSCtF;TeZj`aoM>0ST8J-rm}fWXktxB-8ttOm*4emGwke$lQ{b%9u^%h;zhZfTLJ)n z4A8fB@po@}r1m2@4a-U1J0pIxtKfa^-?U@wPiINNdVdWNU zDivGQ&`qV(W14m-u$4m7%HrxJ)C`gh zn;0a83x-rkc@ON&Tg+OF#^JvAr`9!c;K9fG7(WF{+m?l%L@lOW8&(%ToD*1L~>z;i^=9fH%OX~352yucTek@m#x zLlR>Hoi%-3IPwM$N{-L)t{$T_II$Upc3=+$b1z8#(VBFREflm(#h6&e0YT-geVc_2 z6SLDDm1m-IVeQQ|Q_6p-L|`F2Pa~uNY6khJgf9uPh?RyEv?Cjc+&(f~qhRs3L5}(R zKU4}_G3RFXgcr)zITcra)2MP)`~w=2Nl3+9=N~o^rwAH$^o!~4EeC2Kmwf_5ML4&H zUcsM`G25B3!D1*$Jrx+{t#o*$i*wnWw<*Q!>wJq<7a`|vt6&_0J-9af2yCEV@t3W=10DkX!qHfQv{i3>e zS`u-`{EK#P7a6N_R$QnW8ZUD@oO^*-8PWB6h0Fa9Kew)QB8{QK8Pf;s*b8$3p^5ac z-Lh>dD78f$vBj1NP1U-=y~p(AtNfmNclXKGX6Moew#j)lOyvehX(PYW2+<-yum#s0 zW%UYE`CQ0XIj$&nD~~lgD09e>np2tbL|_vU34wl#N=jpfzeApWtg7~F9<_5BtKS{H zvO%??95`PjKR2~a#h4j;HEJX+IjZwy1RnTA>Z=seXAYP&^i|yixYK;KbN>I0bIj|>HF^#&w; z47^O!`bx)*D!Lz>91kFI`f9*zeDQ==_pQ;(BDM`NU9 zLjUPNYmYU7PWe)nV#ChyKIL^bgLAIhyo8vchqvah)NL zJS%UTa}ac4T}kNxPLRx&85n^y?ZB((fdy7Y_j_8ZH9 zl&c+OzUoZ;%J3OM9o^YmR}oz4LSrdAwxFXHDOcM@w3i~`jlFKOqOD*PJd&WAf1XPu zJ^!MF;AQDW4(TLU^pyA%vVvNhu=jp+!5L15ky+RinoOF!wnKND(q6c&JJSCQaR);E zK}>KGlxcf?jV(ojc~%|!yrB`@4RZixHhTOk_)J`aC6!?yo`_5ztTPZ<_lbtv6US1WsK?dJx2Y94bZGQ$0y zB%s2_p2KY6FgU-Fl=J#m9cBH+ct_`w?mPQw+T5fK_31RZ=jvm+K>m&t7b9wbFR?dd{Pd>P?`tO)~JR`a`mO-E%>`>Dh%>u`DG7 zDsR-LlKqhxJ-=%o%XrP`7Z9T~RW?`qi_d?E z%#Rd0q#34QRN+9Nt4`$?** zj4l^H=<0($8p@@{H0>pd?d_rd<^F?eou^EMAA|qH0@bRBm`vqDl}QWg5-L_a+e69+ z2Xew&>>6D-C`SH`2OGQ}v4dC4;I2gf;Gs2Z+lQ9~K!!&(=DOF7dF<7yM9KH=M~cS% zSdA5&j7U82I|Z{GV#Aes?)&9OwkmVnpz%)TGW6-#1vasC>B4W z%$3v>y0C_ou%$_L#>nH$7Ihf_7@-$->oFJY~61R2teo$oaVreJ-Naagn0|2SbevXJ1GU~if;W>Fl>;Ys9O2zEyR}aU@_&=R`^uWA zsEhAk1GfM8qJy8YJtV&{mf%ISIivb-p7IbUGoL}-K-pE^fP7B4;++8You<(|8*k9- zNNW>pQz@hc&FX9e7P)u}xaLdW3}2X=$|xH+l(Di3do z`A@(<3&T_Zj0Gv}OHmDc<3^s;TOWtO-lJ?mQ_$rpW%!={x$OkIgM^UMpY=|2w4uFy zwF<`nH%#L-znOkZ%Z#CAXf`qEarADs))_GJA67D^d_Vv!^utS=LLZmAL>)6#r&(J@ z>CUw}z-vZ0qs(ZtE;Y_ZF@l${FYRQ$EI_-Boyn?uy+~ohc>W#oeMB3*w$U-f*`SyH8o;6CROO=DmC>GeqY%ONDRN=o&LqoS;I(yk@!sb}-afRoDe+*<t{So(`8t#fI0VLS4#!*kNUdLfg6FL||=R37;xih@NBdbu*4yY?FV zl6>!|tZTOcl-asw4J%slfyQMFc2v)JX=b2jysYMselHLU&^%1pldijIgvugQeazwp7R@QFU5u4cTkW`Wa2YIwmOxsU!~SO zyfmW@XFo-m8!KDpFs@Vl9@W;W`l7Ng2;fm=E=u=qRNKWjpSkd#lB6q^kjK1%Qe<8} z9fiI)w|&J`rWexB_MA339hauLXH5UC&#Lj~6q)nsL(pF;=)vdW$RaiY-Q|_!Xp^DvX6qWj$awD2`m3%#ucE0rV$fu{3P_=IirMK=dH&+rw+{$o*DaUpCGyNwg~Gt$ zm?ZlozC`pmkGfRUH7>%ve&Ze1?HOJub>b?GxM&h=PxDDDW1qma2s_z+1zg;nsfphcmt&}&s zu=z3&%C3T~ye`Eo;V}HIj5IFVJQH!Jmcl-H{+bhj@@rXVHrpBe&~~o$@WVemqU#qz za8SWLU4W0erM^-#D|ZO&5dS*fLyyRmv^Ml;Cr=ss$BWeZ(uw4kgvFaWul%|~w&B$Z z!%K3^=oR2Z!irVO%UCUYVSFmh+!WFL;QiU|R<0xTg$1f75jNeFr{TGnp&P#zj$Tg~~5Xi(EW6QLi`2jJA}04ctF{3e`#xVO5f% z81mpnNcetEU5zAnE55y-a7`?0G~TG|P>sW0$}TNxKu>;$HS#uU7#HK4x0C!n+jl?u zi9a6}>&(O8`S*3ESb3o||?He%}I zh<6r>>jH9nk#!+Thlf0SpII)%KBvL$m+`dFd&1eD%=awdWk1+W-wTO0?#%LHWlL$~ z)np?ejk};u8~aj}@=H57kJlt`K*gqn!8#A9n-&Ws1RU2expcMhYfQcl{0|EQg;5*h zD3jk_aaoRQmQao;Lf26INp@E&SfCfTXkN zp^MVF;E$J4w*TH~IU}LcA!mQf_vXX=2nMQzLIU_XTT@B;W!E_y0a*+zPImHf$r#_y zNEd3mMTHNoHR;A@4pC~jv`=gsDwZ9%E~D8zl9ei?`OunS_~C?~v|l?Gdf@gvwv|7@ z9tP)TnU|C4FDgaIU`AvK`~po^mi&tF>!9(?fn=!l6csPQ)MbdLqbP*lMkX>7#@{;y z%qkWpr6l$JS~TGLFmWMGx?PHuhIb)-dr#EdcE^EEq9Ni}MX=~rT)IgSf1_LscP_M5Q1hR?> zq{TA1eb_4%g2MHny~{5BUl)qlwDjLXV1hNy zHzA4wv(>Z8({s5??avXuz~Bn4qc^Uz4~riTZ;0XYxCRj%_6NLvh8ZO78LgRa&1jrN zrQb=e0-~6%?2s(}nqNsEL0f*|2zLe^O@h4qDJeegM9+K?9qPG1Kwe-C4RI$HBO$GSj&*MoyyAmo_Am}nlsA_a$cRq-Pe6C=W{~1 zW%WP!kw6KS+#=H+5ME3Nj?$0H4W;>wKW&`JpZySq9mn|0s*98ET16-JazW)0iqdBv z>gsMb2OJ^R5cYZ%Ve6KfG>t#)l=>g2@on_;fsq|LjD_y5NA+@0l{L#l3-epKu(Zws zUKU+zX!twU36 zV#_~|@v?zYj(axHOWjDf3|RdtOu_`ij;CS|HbpD478ZSa>n9?JlJ**XxT-zi3IJ#V zcx}!bjLw?n`})6E&mOwa{1h!q9+g~G-O|E+*<%TT&mc2vjpl1V+)kyPxB1x=dkmy9 zKF%CEv8JW*Uk*tMndU|j8s|e3AAQ7b2;nM?%vd9yGe_qs&=}-Nk!y-^C$^|1-7LtN zy(0?KkCPtmUEg@4P{J}*GUj0(g8%_J%s2srM_mzpcJlwy>@U@h_6sIIsz#CV2fnyZ zIM8jx2P#tv3of4(;W%QP`~B8FB`G~2wx5A$R#*KN37iw+Bpv_7yDdS37Fh3<^tCAP z>8RT%V6r^3T9cmjB&hZmmKso)S%zw`9v$?b@q{wNz;^^$=%B9R(9jk z%uIC$lh#u?gK8)JUgIpWVl!5Qlv8yNTq4fqj}2Gj70P|Rx8~Rij1vJv(-i4#6;p1= z%I?4ncLKOy@GL`cyO@yYTq)rTbB@Oy6Q@z-){5nA0!cZG&IsdcGqRN|L$(pg3b?*} z>tE1wPDBa^mU9bh+C%V?Y%$+c0GVEt)U`T#O1$H9m9Y;>Y0ep`WZtytBgJLu!}vC! zu@?{G@zhQ!{5QrCCc`ZRQt}mU-vEKJJ_J zDez<0ByfaB{P+1sb<_bav(k2Pr7HK<8`DTdOa99$M>&v$4^kk(&ZFxk5!j?gbqR&hrJyn zQ zv6rotNTJZN4Qn(u+YxPrghpnHrY zyG(qrR(gfBHfTX-2A$HrS)Ac4uHa|1w zfQ1{#O&bjvVC9lhX@W3#iG8Wul*g(d(^@5w^t55a13h=kKl%wVT1e0k)iDxZQs#=f zv2P!vO=T7xrn=HyhG!)tR(Q*CxT%gwoJzLr8?ZVn^NbVn14~~GWu&*|an)1UT_@yE zO5iJqCg$>197^We=n`>_X)O^>uW^Y+Ib1af>t<)3=OdI%e0*ryiT!BxjdQcA!}H|y z8t=aHZOKn_tY|U6=-|TG(ztaX-*5x$w^EX|jXvAf$0}oY{D;*pWcNqnb1Q@1`m|c! z$MUDfzl73%HEnp?Dsd#w_0$_Q);x_dwN_g!;^>t6uFF@tlpk|P*b}9K56Da)V3E)s zGUuPsekoR{@%|;ZkSons-cSEjzLK>Ly~3ea6^0>5)2cjeeZ&;;8AAaQP??HItD$mUL;j#BB!BQ8hDOvPGsd) zI=QHlEf+}sH#&V{fO8Zh>|B@mdl%7%0>5$3AKSo+n^4!0JV9IQ;VhQ(>n7wAbv3rv zWw>Elx4zE41GAZ2h9If(=;z+siUUveKeSTmfhoed$kU&K4MPnZdga0Jzje)2It{^< zhjpx(2w83_<3Lg2?Oc6YPU|&kf(u<^Al2q4_;UW^cP^5>9WfCf1~!oN4Vgk`VF2 zz2)(fji9&ClzDLeM(=UW zmRD9*zd?QEo}WlJ2XPvZNxd`93))1*2PD;iH9AiAC0Iq)gT~4#NV%4NY_KL^A@l4^ zBYHqF9s~0yhC6_C2W&`go4YiHw^^gevW*kA(6egQb$t`V)2d@$6to(Ps#?1_H1E zquQ!03Bj7Ce(qZGel&ljZmMH0;DNXX_J>Tf$p>^=9id5VN_$@g!-1M8IaF-f3R;(c>+w9cQIDLW1-4WyCJQ37!Zm+z?AaT7Q^?CGG zUGkY`xZ%8gZsRkgEQ=BYN2AVwB=eG?M9wnbbJc5>N5y>dz_ocS*}Qo3i&fBOd~yRq zv6yGKYkPYh2jaz`DilhVH^4UC42%LzRnDFoDyv&*`}whb&qv8$m~hZ1Zv_Pf@$Tju z6SW$e%V{$XP!?1@yjW>vctpx76Yxc`7^EiiUohboDfC(7PLN_moeUV(Blyk9YYuwt=`J z97;Cz7(-I{XKn$MtHiFe$kk2SyK9_=Q~d1=YzzNXN*l$o;?4qHX?CpCkrdGG2su5! zy!=mH^g*=L^)MbC)FQr{Ee?-F7iD5pS(ghB91H3I@XY-N;8a-#C!YQD{UJ;A*2C?+ zy&5`cdD=3XkaIQxpI$Cx!%Cg>421jZOi~IZ%z*O_qksARVgH^ooabjDKcm~kt>)FI zZ#TuQZ-y0N$Dcy}y=uI*$Qrj3dWw7Xif!pdGr|T{>cd>I@@_%kAmqOJEVb{F@DK8! zc&x{3H=EA6Zi|Vn5xjI}2)?_Uy{}(?aQMR;xz0WQvG^^Y>m^Hkytb~$shU*vq{N3t z?xWWae6oLjKRSu!w)+2sUP-~w3~MHGM=os<^(Rd7-J#6`gG>KHZoN)jg4jD2Zo8MP zJC;B$@?Wf0qK^Nn*lG0=|ND0)P3~#>>lWEO_>i?fScCa1ql-8Q`~R3^KVq(^dn5Xf ziIx5m``>o|xTjuvSou>dz7y8ne48{EA`9Is&KVO?OHr*2Yerb>J#G!!t@f3zYU>Nr z^GJo4*a8l!?b!k(Q*FroKcRK6*EarDgvtDeW%a5`ca4+vtx3rJ*=+>s&)-gsCnY9Z zVTWhf@NYgk54~C5NbOfI_nhbdY5s?0^v}kE>hGM~zx#-HM*=-ReIgHT1E8m`Iv?&X zT~<N@5u_7ci53(n zM&46m)A_I2u};fEW^u(t)<&Z2aoc97xVj?!G4OJq*-)MO! zK#TiwRF<~xF=mVyigoB}Jhya0=96IWBJ(33Ast@f?;TG!rC}An!DbFWFFx*wZ&7PI zXQ#d}s5W&D@Ph`jIHoKyf%5>$#_7PnA!UZv;|9f|wqI)h!-A5#){Cj3@SnDwYPi#)7(3ChkqUad`1BN*4Yp5BJ)p-~t|}U=`0VvRkaaYV3!V?!hp&+Qk+5 ze@d%F;P9R(PoABL8N+T?Qh(C1hHGQBBbVZ`7%dt7Z_%q-d|=IX;H6-h!o6$OKol zx|UP$qzHJ$X;hYcCd*4fODJS0x1&0eXfKfOq^GNLu(@KY*v^$yf+EVxW~G~tb&g95$||J(*sTCum_kT= z)1GMrzB!vn%*%;}7u0S}p(*vXVBLXbXHHIm4XFt1T~&ady_s4Sb=B3}+=hB|g_Ssj zXO7p5#ctxBTmR>ThJVTu$+w17fc%(0anVQy(uEV8a+ScbXb@p}57%}}Y=wYquGgbdfHmf##jU>?C|btKG|lb`si@2P9gCkFI}X@O1~tfK||dnxh7@4Jub*P6M0}2B(jzMtF2$T4TJ0(xi%S$ zN`-y%(SDFJ6VW`A-WUFIML0Dr?XEw7Fi5uEVItFje{ai6@NQ6z#cgKkCG@>lGM;2< zN5nAEszL(Jmr7BAfeRm>uPMU_L8+X&75GZwp)9X&Ho_)#@0U_eI`Uq>ID6_liSDu* z&85#ARVa2uqEfW%^Oq~n{u=G&29_PHIxBObVc^KNmP2fTohk|u^@X^zC$-O?@&F7} z+Rp^KxtrHu($OD$Hz*gM1yDwr=zEZh`wgQ~0b0v_B%K64@K4#!Q!g?w9hiXnnE7Ne zaJU?+hJ1+)eM$}5DnwX$6uyRU$h97F*B!N{p0h0a5ur00oKNXGaBuZ?DZJ|WQ$rq= zBV@v4ZU}lbo#hhV6*{AAbm=8XqZvH(&F9n*)FD)uh` zqm&u$37AJrx_szBjK;Xg&(|&)K4bY4PRpK6_?ay*_VJ}whcCnwwHzDB9mIKupB02! zT21jR<$fT$LV}7$G`Evd?^(Fm#5n=R0--bO;`UZO7GmO-V-gw<3RD{l5{hm=F$B`P zz0NXIlZ)T(6N=Yg`1~$s>%{kK{2RO0DtIW6F~17=MT^r-vh~Xqj%+V!_Oqni{U!0! z90=2NJCTI$D4PoFr&smKNuYwdnY33|#-hq@lg*A35Y>z+Kmr)YC_~CuV>Amq>BSo_WZ9WN2C8Ioe9{ljD=p=T*@ds163@zi)7wyPJbHs_2By~UV^YN`XgU$Q?-7s z-)_U#ZY0ebL!l$NDHkfeHVM;-IdTr234Ba^DtT9jy{F^wCu=LFvZL=i80k+3N<~#3 zoQ!{NGu+cm$&a@8=ahA#HROLmUAI|{z4jA!O&6y&Vo;b)@Ue&lG8b}<$|QY#Zyu){1xy9rAz(e7_|IArJ?IVk-7(7~G3MMIMdPTP1(>B61KP3<{dq^th0TFz&P zEYf>IVcW>zUDzw3@~EGG4RcydtKHw3faR%94Df-m;Wd4M*${~@1?G#K=KBTp)>p_a z8B^)BEu+#AdC-|ay-2aQtnT1Zqo6JPi*O*Y5!}|ei~CEV>mUn1JJ3LcWftO7HV*o8 zF570BA=S;=GZ`{J(B!vo49s#mOAdW(19U*)flUPhJJb^Mii&y*3`h}0oFq;KJ(M4x zy3@MT?LOhhcu;328IBo;?)QTZRa}%q&$@5^- zF&9=dnZ9V}%Zjc;{BIk7=~QMn7|Td0(I^}x_`I|GUb#X|$_w+4i{7*pbJ^K%^i1$) zFKA@x2_{l0zN~>#`0&2EEX~6IG)*N4Hwi1%kL$n`bMD6M{aT@Fr032oNjOcVLLPin zVp}VRtaEIdTG|i}6ZM7@|4Oi9cX=RTRu#Z(L32~1V*9`>U4U##I9-{rcFFj`cHWQZ z(vnYCu-G7Pt1|6TmGnpKIX2r7>`pE*7;krr9B5f0^_Q01s?Q5F;+pcsm~=-pWDdO? zWgX}2|NOSyF!g%Kn-hDbC@A#GM|_fDEG#WNzsLz-z>;B^(7;B~w80@QqFijbs3Sg= z6#N||zP_sKZGSmn^+PW)Vd?ytUHJj;k!MBw#~N!@dpvL+PDzs_ox6^6(dR9qz6FCC zcHW{%#AAJnXqR}SA^OA?xg*%si!ZO;n8|IJN?RTXTBI8xvc==D{z>h~x`W%spODeT z`WFONRW)=}vJy=U)Fu88%UhG@$qx}Xn^28QR5%zMA%mpCyHQ@gI#VY`b@g+)cr< z;Z@{qbi!EK+b<@KAr=c=nsJW==l>};&e$9& z^-JQHzD;(2gb5_6jn9V?i(L>fP`d5dKV5;r-G4AnT*tgHb}a=mU);$1>z+>#Xb%@fT8?XO!)Ckvui9QBvuQ8 ze&eM3=eXsXuDQ+RqYg|Ac9sj0-vw%C@I3CPC)k#MJhDjq&sUULag=Yh;qLr?3E+A} zZhnHRZTp@)JQT5_6RS4L)k?mNz|91a#+0BRRMPnm>i>LZ83WV02qDeufEfNj(US46 za`eXfg6r8zf7==2{1S#L{C~iBesgW%*?XKRZX+2)#2N#o}|A$wes8x^ZhSHC$140yRdWfLX_QQCzHP$I}x;zj<-+a+YJ%X zj`u=64P(Qm5mKugN}6Z@RXs_x%IWT4=l*XoZ+SJX zJ%WKKwnhaMB_;k~-S|fQ=l`(KOo5M{^ZY`Yj8(4gZ*x4uJjzU@8f|#ed&8Q6vvuMUox* ze5DNu`zEP9?@V}vqWK#+b7 zG?4gR?7okl{adUy1(UlNIGDxq@(aMXdQJB}r7jfEX<2%B66AX>C1<%WSwg8Xmt~pN zN$!tE_ub9;mZfs?PSj7P4k%m1L}pyn%e#W1?mF>E(xOpQL*Qt^VIGm9qE@wuBFal zv}rkQ!UH{X36))>FSn(nRq-7DOR>o-)fU;9ekdil9jbLLHgWpi!>4o zKdN8RvX1W-jxL+TR1D(_mK%*LdyX}+P2q)c)}#meZ)b7ndNk)6ScT^E>K=muByi3E zH5bl`+DiPc@WM-O=R91~?nfW}1IZVtE!S$UTco;#?XpOObT4bDBB-6roI!Y-yw)Ibq zl9r@azfa8a`@yWGX0V5Xm{X3;7b$gfLr9(0T&v2n&(%HnupWOvoc_G0^&+q8qc?qi zNcyw~+9Oz6Dlt+lom#x0<>l~Pvgf8iXB37nT4<8+$hF(t(Uw+@Rgs2LI+kKCy?IQy z4GteWEhO1DwML_#PS#YVx!{kOf5~BTtpy4D)L4ARK`~SntRRl@l-Rbx#?B#=xrYRf zd5Z6Sx6Fk+-VMi6O#H6;yMLFr0->xGOw24Q-=NoMbPE^m1r$1G(I&N)*J$qLx7w5r zXRm5~U~m!6PTt=;jAuhH9N8&VgpOzns& zjgtIl65S`fByD)pcJ8f*THF=QOiu=-6Y{fEzje=o!gms>UW{uU)~D;sOtw{iTWVtL z7OCVivB_xY0Xd-0qA8I2u5T%n5>Nu!jXITX2NJD9$)2O0-=S4FZlZiN&o%qA4qXr` zCWkKgG@_p=BfR=tDM{MQDgJ`!1Q_d*jO~%d`6)7u0X zOEan-^siQ@x_b_Q*dSS!@n7(d8r~Cn2>_@}m@d_ivYO64BP(^p&2dw&$ zW1;qs&pXh|i3DQBcJJvpwCcQ?F1#upO>S4iyz)M)@}NH#SYGj-)48IQMVpo*z3JL$ zc?Vu+9@4gfgtZjQ3hdA!H~JePuUM9@;95J;lBCn+phRxD!b$Y`4>u0(`9sE@d9ZY{ zBqT?{EZXb`A|<&x@j>e8VUhp2UFLeMU~KAHKrVKRqaqPs&v~1QIFj$M-m;}g-{0Bg zQm>=UF_U}--^H)CE|m@RYRe;E8&i2Z0&g^DlyPnec2Cdl<97^w&*Ab}kvGl_+DCbFQA)^8rnV6XKgjf&}a52q_16OmVZxSQ5g zPSS4zYuE5Ok z*#<~5n>w}%wlwlN8IsME^rmtcFOM*hd+&M`3rcsmV4-?ru)E9i#A> zdzsw$*xDzz#BLSRy9pI@$vAYH8WB_3S9NkzMiKhDOjesgk+gk zj~M)*n|w-K47(7t_cNlLSmG0yF^=eJc`xbAr`>lk9A*xp6=^n8GSKZps=_xC{1gk8 zW;$xl%w|EN^?b9lE=v_=1r#F;`&kt^Awg3dzLiIQ9p=AEx{|jVLfmE~wM^n&G_GCE zp%{qy;XPjBJ3id|6C+ofXfIGG$_OfzXK6)=W4qAQw;)l7NsDT~(Dypc*eD-a?|8`> z2_)~AB^vEyDnjGSy^gFA0M7j`aABB{m3=&j(l;B3h#ESB6Ddyj#%2t21|}v3BChd# zKz0LHkk zHxGtInr0R?yXt$?pO2*yKd|}K^5@D*70IOtR@-)h8x*}@T^-o7m2DKz5=8>3X#Bp6 zTj|di(Zv-cS8s(v@GYAt@Pbzb@?H77dMEgYJ&Xw~aOy`IedB9&)OI_O+7!T@YFYZQ zufuX)F115MG-2IIBB){m)^s#DR$!!TjDYaLE?xhHo*Cfh_R$S@AHpHdf|rjLgjGM|RSvngO%LC1p@F3gFS8}nM7#S;AI zRTQS-c=Bz7!AT(Z{gV?K``>S01n`ZQt?mRuV<<_7i%gXRdz}=GPvhOeY)GriLPL_8 zf3k;K#QNP&Yna9v{=}2W*+}jTb|+ac5veHaNtjBElQL?uMo8c+*J+rY#Ll_i*Vasa zGg7OmUm8dQ;HM?$1~g)mV(V9QY7ip*gGTOrc|ad5TftITZN1x+|PO$l(69I81Q-aEpRdO@2-+9Iv@ z(T&8ToK2Uk=*|!zUg^*maP=Kxo&r)mg~2QUbMdfM3!-yjlkO)se3_@>xJ#qSjEc<5fM8of(bU%*W*>RI@xI=5&XpH5_e4koaD)q{yWRfh2KG zj#WEVofr~~V#+as-%l8^jhS5(HD317F|FFjSoU>Ry)TlUj0BLHKp%{v`e zi~}cCLHu^Kc+KI=Ob`n+%e`XgvJ$10J9^e+bWir>T8yl%{YtPlU5maHLysb4jln6ylDkmEsjsalbmZn=xKsLW>e zH0`45;=ykTj0OhHl2ic6fa!$+F*tCnoQ^DKmrE3aEI7?lJF9?Wu&alW5iar>SDFf3 zVkS6Gj_2XT%GS0f#y3B1Lem-#C7er5VyQ;5vrKLjD$FarK@d|3qWq@ofvH`YS*$pN z$$4O+0x)5Zo%s2!WNwlj>Q^H?*%>xVF)KdcipJs=r*NtSTWi1h%8ec;(DISpgS^=- zPUCb!$Zioh^i>)7Tq@5f6`6+|2SgxGJNjWXm#c7GMoW1n4cp{v(M>$jK^5er5S|twmmp)1_CT4dKBjtSR zd+Jog&SXufHP()^dV|lNk(UI8}k3g*YP(mx@ z<%seG?7xol8B>i%x12DgVwx#~84$nhGRY1@#ZM`$Sot+!Q5IPztuXbj1g$v`AoEfgYmIf>>zw$UMnID@ z$y1YxmmGDFN>egImZ3}SkB*c@X0l@PN|LP8ml{u@L}c~Wwc5R8=DlTS0YN6NX<<8;l@)J1TER&KwzU;O1V9d@^Z8BjZ+2ZL_T}&~cK|;rs+1nb0 z1{VsI+mF+YN1a_&ZVdcr&*U-%xb1^SdvY38WMqNsASf#5Sl}^ctE#aNpmqTny`49M zXhitc4kYreadD}(g*sTcpY0PYZj@tX);I{BJ>nqWVtFi{GxT*m>$3^Yxf7jSO=PwP z&f~I$2m149Wtwfs+Ld!SGviY+-Ap)$f%eWGXT}c!8H;6GW9-o=S!tAC^n&t zizMQ4-p87k5pzj{3c8KSe-=jmkRTOM)C6-yq7}76P-?89Z)uQ^zaofmM#SvM6IVfq zvsh!pW;HPt2#rB_q1RF(y`@irv`RN-O!<_qjO-I78i^AVI^P=h(Jb$bYMPns?!&H> zsi~Vr>8lgToArfZVuY|7keF5gmMQ%@nMcy`@%Am3k z$6XW2!BJtuiy@L0s&;%!#U0e%np``@dWIz<#v!LBQHef@$5YxNjZIOWR@C2TO(BUJ zeo7_|F-6%L8*Ydg6*8)<;@njcla0C%Dlr-sRO~rp@S|2d2ozbGN73ndYmyX4Xvj@YmQ1L>Q{B7E( zhy<8_DvTFqDCx@=R|TgBdLn zr3GVoW?h%K9XYUkBYmXY04T(x6l3m>RcN>2ZtL#8HQeZ(n@VSz^-5P2xC8Me!2P;_ zj)7I(d(a69?#;5(WF?(-*#w@rMS98>JcnjXxN_~OP+ucrj3gj209rwf!dr%VSq#Qa zn=HMlKZx%eYn2L*>|5De^u*kjQwa&&naDor*VnpIb;cf(5t^Z52~X-(QOgA7TIl7q za^!+kEhv~bQ9LwM&C0YMoN-6E8fsXegC62@Av$m?-S27lnb$RW26;rwnaMHT1mQ&P z`#lz~2w%ODWlC}nCeCd16*FobP?mj(Wf^=Cxhiog<+J#OLAAjx( zNG-ObB;%OJizW7Ed==!O!V6HfuGhRr8$YQ9M)A%_$2~oKcYW;u_{2M|*zNW|c!)37 zAU9eQ$EE2t4-jWCUx@qu*#{$L5@Csr$RLGen=Z?iERqqD{{X16v{z^iuJ)>hddz`i zC0l%>m#Pq@aLA(^sV>cV5zB>>Q!=}rFjpobOEC5`vMn6z1$s_xR9h9u6QfK-DC2HT zA7@gk%A0Dt{Vy!Y7R`paRNB@g!I#Fh?KSxL*VHeDs>KnPACB zMy*7-GZ0MrJxO52=~}8eTf{0Q0@bO=b;l8uA>@Swc@`QwXuE7M3FTvRnH<$xR6J0QF$BS#9B;Zi$HYv$hc9evjUZEXg7qY3exS2jCMi9dY^D^g zOEqaXXzB{DobstQ2w+vPxMXfTSt~e^vdCo3!>yrkDa700J6ZFKOiRBq7}chF+_Op9 zlSTF)@3SUW~KXi zNXU*-jB+qgHMK~pjt7x{;%Q1AS!65YTa^}r5d*YLSBV=^%ua<|CS+k})h}6*Hei!P&i89vC!lv#2=t{5{=e|+o}h`G&G#?Y!Gd@WZQHE_CWZ!p(ABFeUjAqFxm zD9JeBa_7c(bgj84_EvpJ%3>sNsN9%&qxVQoA?Z#uODpW*-EdfbFu_JhR?gG@>XmkV zIb~lSXvy`nOcw^#qS9!ddTk<#G1#qgX8wsp`@ER)V^ds&!%+(wtvJNbW`x>c_L$r4 zdJ=w1_21W*=wsTRyXzj?_ebk5^p)+ebNg@AdA^P7o`vgQs9&?blk2{N>wcGNaXnwt z{ae$0N8YQ*;QEX?Qsgao_EA}KB>Z=px}00~f2n(neQa1Vl;d*;jEe9AZNl0X65Jv*W5h%Jihqx(A?^KhevVP2IS&ui z&)K_w&Y@N)zV|sL5<4B3Dp3=|tKn<({{YnI9V6J~L|Gc+Vnb7XliO-HC-&YYCm|S- zUiaC?Q(tw9CN*u!VA`sG2)4P-{7&n56;bL}{D}U55#E#ibN-F)!oeo*`l0(Q?9)L& zLZvZ^WRD3$n$P*Py`Udth) z{YMbSjLgKd9(Gf+W*gUOh7psZpz4wIciZ2&zixi+`&)rnrRjd=_WuAglf>pzi|Po|uAGrcTVZV|851|T!sCy)rDJj9lWDov?%7`g9$y` zM&q>OI^ieQ4TkFU9gDsfCoHVfX3YMk7P1IS9GJq*H+@GXy>gpWOhQvJPLv3fj;qwi zI*cbILg~Vr?9G(9l?H_qt@m4wGK{Z0ux&G?C8C>Pt7u|G$iFTFkk4-uLD#SV%;lAK z&yt}-pQ({XM_uqgArmMhBb@Xa-9!K zMG@H;S=)Hdk8Y>z&0hf-1`Nd2P7snpy1F-zRm8?jWR)2*E2TeDo*bp!JXbQRn>~$E zj;&Fd88~7NKZu(V1bJxbpN-D<5TaznrG=qdJ>MC0&V^K`BaEG)6r>ViyIm4&lyYr6 z3hqCa!hl&z|SvgdJkux14BC zP7Sf6-loRT;x-EWS2&$Sp=Ve}c1aXbr51&yFx4w)vW=&HY|kK>>nk;nQ@P1;y7=q> zPF%R~)`kt5B^+k;N-=O{%uM2n$fV;qzLFtPV)VV@Q5hvprl`t=r5gs(9IX)th#by} zh_883DsC2$hHN86vYo*nk#c1Gg&M1x3}*wyioSuqn}|oY!wDo_hP;>a1*rXX4fRGuB~cQB({FZ=3=c%U(7S3r!7c(Yrs&S?r&kwnH+ zs;gn0n-ol>k5FV4)p-nCF;O&Hb5*kn9d~4GN=& z=LSR>p7XsM9kjJx%O}0TWO^$ZNz2YGDy1Hi@?(W<-GBvjLQlgkF=aUNWCUw1tzFF; z?Os0=gu5PYQ)|2uLnbQ0%Z~t-Q!8DdY}RLdQJQFIXxd>ph!ypi(G_IK6G}>={Z!-6 z6ol`%b~A8?(Q)=4y^{_7~}*e}ZJyw&G(J0Ek) zw~=P(!pZoki%{0C1Wd`UL&hZb?-n=WTqP!w9y}6jMY3tg)foeqS_P`;6am zl-Ifutu3D15BHt0M3KpxA5uD&mv4xj{2?`%m!&$e*qHqJqas3YI|d<5TIlmeTbj_# zO~@d*+Z~U`QXDfXy9XUk{{TY@a6!uQPA}#1n5Rs@h9G5eWm*MnoQEaDTSBU3g^gqn zc!oGD{ADk>kM4MDW(}X`ea_3>N*SyQ$rgk&79R zaC$vFyPoI8Nqe@Sv-Y3kF!maG1)zqCMfS?K*lB5^g_qZnKxEIGjM+b#qt4 zfgo9&&I+o977{U#pbp^~Tmah07FQ=^>mxYvWoH~>R>>_!Hva%G#pR{;CjKVsBZ*o? zCoNN=v7<9eOwUr=O{G1Q8K2)b$+^~`vgJck{E6u`LX4H5y=``zX)%;$q9uEj0(Qdx z04$y?1;%G1+qqMlxnR^mjdHEGV;wqWRQ!+AzL6@SGIF%_hIp+6ZA|XK+3f)CR?VdYTF0yYXE{2XGzIQ291;n@!w9zEUU9zrpl^V67wnVUJwD(4X89Pf|0QB+xz zHdjB1WaF5tlc!Y%FJWS;uS)>>D$tvw%Gx4#JwI_puPSG(no3}h*b5!io5+RY%NZ~O zJLqDHPY7`&#j2DZ?e=C3xh*1QHR@ubz?K}4o>cgWE5oX@l%aj-wqi_6dnoK?5M$)6vs6ALZ+6Sqsa8gTKP84cNQ;IBzJ=FF-Cg8%hug^zoGpnnz#tr*`y{yUDPe^&A#t|x4A^7fp z*&dnr8iamQvccAA#<7nJ^GbKJ+SCymos&S|aVpa?88Z8F@y=Y6taXvbL_u76zUFH) zNISRA;v#nf!DVw}0jSg@5O5UAA_HCkJdTrAqJ#*otQRWNe6j+rr*pK8YP zodTf$0D9@yT7@V@i++(JFU3JM%_<7tza`_-ROEC93A0U|>q&yUH(B#J*XgjHHhQ?` zD`a8;QMkA;WltVGGI>%hrD#zewXda_b+Se|pAG0ay^qX1ON!&hrhmc|DAlDM)fA|Y zAysOWKn_h0B1nII$$r>Z26-BSx3%2*{>9Wb@4xu3l^L~wHFjQY@hsmGHXs}gUsXIrIiDP$Y-*oxLkZ3|L8 zCTmMGIgRGAh=8Kg$l$ZErFf`|s8i&s%&4cwK2P;>fOyPRG)&TVH{lUUP0e2e7U5Y% zO$=>VR2j*TS2izk1=33Y07}QyhSFP=8u^&gzQ)au7zArfvF}uJ#4Tzjlu*uNNtuw7 zY-KRUbRDj)&-8H_nZ-WeH%()Th*c0`s%*9&P{-nmNU5KNFhf-3=*h-yjcFdyG4&C1 z8gi4B4kEsGkeT^Dn<)%4M7-97T!T~$u89SM(a((m@-7!`dk`yY^YxDTV4RgIRz8vA zXJx@oEpt$;d3<(PGx6%O%UINO=O$S+R*JV-R)_+;A`vj5i$Pv;wIT?6@rQAG5G5tw zkh8*Z?i#_wk*f)Vg39D3#FE8WI6@Hez&P?^%Ud4o!9*QJrRg6v6s;i*gQ^}Z$0jV3 zTPn*-MkX%eD^qBNI}tzLylbhL${hlWCMU^T&S_1iqRw0X;zcGX*g%rO8yXZ1{{X7! zV=K(M83s{7z9z2afgE2f>hzt6eHy@8~{Ol_l`B1%Cqot^F*It@G6%&edA=tYpraRqWU8Su4JKwst77=%qh6yB`Bz=HF0fR|8r%z; zM8%;6r$%L39B|M3nW$=29pAENvyv#xCgIUqjJ{i7(``!2<2OF_%ecFw88kF;S2yp({W_gZ3-E>g}V$zvl<%;>7HAf z3H7OzceK=D1sPelB!7I^QZry>PGv|$%A>;l<8b$$D@rGt?H>TjWo*SnckJB9|nure}_62{xl1= zjhss!WV~xKbgF+CK+@DT5cuLxk-e;0R~v%FI73cWrS$oC02zoD*1qXdsBvS}SC3kF z%$w~{Z&g%Gv&cqOTPnCFYSc7lbjtju-dm1Ob;!k!MOrc0>wMxTj|$DKpooyIZ?%Z* zOdFr8jv~G?wwWzcx`>$-r7JqfUV(^(tp5PLC&qP{r57tTSFoK~sa{E?%5osWnPaE{ zH~xsfE82bcXNpyh zRE0D7n3Z!pTf7SJhHUId1jCGYBD8`A! z15&6$F#<}{u7gvOSUJ}_eFWt_OlDnYUIvZU%FkcaQ;J3T+iEGz61GU zIa?;%-@w+Fp8M=pX;{pmmEt1mQ;vA%l&c=PO{LwUMsq!-ohATM)isZX{{Y-MLz%Wy z^>n~M_^4J>r~d#tTdiCT)kIaXXTl9h)+r12#Q64H)rq2Qs-2EAqX?HHQY4idB^SPt zDvcpc>qMnlB26|hW_Oq>I@A?U?wR9rj_`4B3s;@QFre0*s!V@O7>-Yzo0U^>?eWG( z%5aqHsVZ%uUKVljxSG#C=TAihsf4JDqIA(E$dbQjU3HsHYGipJD>qeW8!SY-Kk}ew zq+hqkkj_cSA|dHwZW>2IXII%uos4!scciHGI|7bemI}5Xk2NzMGjXS_!AaD?i`=-$ zQ1NjXHD(q1`0Lo_sLR#IVy(ww^MFOOxBviY{hw^D!Ydve*$kL7DcDjk8`T2EOE>-( zrT8i{>O@1w8D$ykX;-xuZxCi_Ey|J{Iqa;^a8P>PIOg1x*4$)^&G5)F?PX?ZfXkFK zyZk8BJ8Ii?Wium|anfasnAN!V`yxBd?c>SzaBTx6SRRfKSmZwIc(RqlUEDy0d78wF z!htGmwE{d;O_s@mYz|!~*i_b!q@5#)#+;8X8-7M_?4*WY6Y`QAd;VV@D|5*OGfO<5;arWBfTFg z+fT#qPH^lsWEmzhV_h?oM4i;vm+X9Wv>$mU)SYO~PgfEcu~&(VW}KeFj($z9)@3+y zREDbnP_nBq*ZELWib%~5Gg&fAFT9uFIHAhsE02B=ALrxgYGN~5GD6CxG5G?k9hUw( z>K@o&%|zY&`nXqBR(dyERy3sDR?RttkyfiCj9unux(t;JnuSz#WIg7TG8l!5H5H0O zr1udEeWJsqmi_6?X>1jfM+BYkC2?Zz~4 zIW;<0Vf0BMdg^2YtdWO~U9fAxUsYH}c$rDaMedEx>S zsyyYKdWc;eDdTXqKL~-XskBFO_kHR)8C+0`3t}WKMC&uhfUg|L#o175#TPJdmTf{e zW3Y9~kGap^A#o=)TI!UyH@-0zRf^k+T)Eb)6i1>$UL$OOQyGcBE_#WHl+7%HbueLE zML^7EvVel9vS(>BYpb$5qZX>o3?D=&s+6bBH2~_`HrpIX$2lrvYSz_nQ+EaOL&ycg zjA^)2Xb(Z2j4aCxvQ+RhCNEgDAdLCHib9E2=Os)_C`TVGt2juryGdQqqQ;f)nA(DE6R3$pce20syC(*0;5M#a2h%%jWtuzJBOky83B+MTsyLiF! zpwz|hiFa`6DzTj|qJSeKtfxyZ(+ZcHvo?6uWi2210#oYjD+F0X6DwR}MNeK2L7!Ef~E$Rw(CT`Td99&SNyj;peQF}FPD3viTVk;y zmIraUNz4|4E}DRnqfrQQF~$U+CLmEieD9e=R9?s=WxN(JJDn7u=MLe03NeL`uxpx;0O?K1>`}>L@vPD05a)3>JxnG)mHY z#BK&2MM=pJfk*O)GhyC_{{WUP2%?DGZ&6nLA^P57O%Zz*uA=&MvdP%dp!%wtmUTg* zlBpe;wr`aG0Maq6nDVYzl3ee;HABJjjbEy!P#mHw5U0`Ph11EHq#ac9j~!^-azpj1;lY_9N_m}x9j8!F(}cWovf{@dO&Dat zgA)pI=}R{yVk%LftC7_+H9n?eVsgPy@O@1Ok#CDyDkjlCgRZ7K!WuJu>H}0*hPN42 zupKhd*2DK50p;5^TL=C=T+HkgsxZl!WXFp!gm>q~$xPY6Ny6#Gz1uC92uU9#P|Jsl z9OW}Prn2K-eQIWTJ494$7$9uzD>LiH8WKU2IQ>Kw5n{n;9C2yIp_^rES*14$%6x9a zR&}lpaG2=B-bkwF6e&q^7bz#Dv_MMXOc0?0fI2Fo7xO;p(9WlJpPE{2lXQhY^LTzR z`yCu848-y4$R;=0h3JW-nA4R+a%comP?FPC{{Vj-n4%QqJvJisFMyNBP6=JpqI6y^ zOenVPh(yJU6+)kH%M`)@=*_l_z*82tKCgoN4a3wBOH7g{Dze{`Ajr!%m84A81xwd8 z)=PvNvLj@-QB?|g1F43w;h$?OnP}_x(x!zq@~sLtsokwNxXPYO6tS07Vm#whsndumzBRldnm1bjrS!7PCh_T$;Y zTB0h^P~a^I2X<#0ZZ;2sl5)wEg9_E30 zSl?AvXbT<7v{9%)%H_|5COK$$m37IWbux{d-AeBa|E|m;!#={pGf%Osp052&^ zD=bvOj3*`|humXbOnQZ>6WYvGsE+e&brC$`W+GLhW-|gq6mX_&&uejMhg4OVQ#wwR zXN?&fjumud?Ee5y!H3W&CoW8lCpaTP#MhCGQ@KNu*DJW^$v3AN32=d&laGw4i8(~6 zRe!os5DE~Y+vF|1B6WO?&^1*}r>e4@CR#M)RMatW8JWt6<8uAN$Um#U)_Fy#S?0?O zl+k^oN0l4^ceeEdM;jB`pxAx9&x*|hBSdVhvoZx^bi0*n%wPT2AF|*hEXY}@Ucv`c zEJ9GaRi3J&kn6~$7|nu!RQ584{>$~Df%QGKslT_BQ?AvK;AiQ7V56x#&WTapWgKB0 z8se|DGp_yPe^ZQGQQYIP{C^2w0wHrOHJvtg45p8wN`NBMk=4fIIPlG*$70&-S|NX? zD%Ucl6$EDK<6H8QPiWc8V+?KGiPWZorZChk$OdG?^44;$m)EgB+v4$z3hp*dc%$&*D%Rnqd%alOj?KCO!1<9NrZ6eP{Q zEsej`k_m`bT}TAkOAaQ5iljpp>mycQ7_J^Sn<9B1P{Q(X`Iof?UmT~)aGhu29V7KG z8QvU;Yue$-J|%Kfg(;n}G)l(%RphN|DOnIK%}7i@D~i>(VU;pnNu3s6G!Hbs7^?;w zFki?*mNhXMB4)!y+s#?@q6oAlEjrg)iJ3e!HR;&sWn}9{LmULB5vd*cB6yiJeW1)5 z2B{dYqSR%YnUNH@?vX}W5Y>e2tn^|RtVq*E*C><)llesm#@<7gr7e?30$Lp5pT%yp zSqwE=zzPf(=|9zs(KyyjxO5+S@5)iGF?8Z{SlfG{GCrb4lp#+9RxVMSZC z@*3saQYvpE6h}_VVO>VBYOCEV2T$NEUD+x;Qcltnn0I6fG1gaBRc7#HzYsIWVUfV|mbJ+u`t~0iLRSv83|49BZ2{ zTsyy8a+H{_Kss8VLQbdMi(yH?K2#vnkeK< zg#3&yXA`;xS(eo)9>(Rmj@b-MfWc9nl%Z^?`eVv6n6@>I<6Bmmu$fr^B`MQZk;vM2 z%*AUrj43&KmovCKn<>(mb*CPsoT313NU>k&DvzsPJ0y3U6z1cPM4&xc7?v0W@53dc(P+TvZ>YCyw)bXr$%D< zr-XvSJ2Gb7C3SMt*@I~(v#L?CS=Reht*q#UKXY|$g zr|f5`T4_(X-oo|&07~?4Q07ZKeh0aDa8FeBf34ZXDie>z_je1?Z%`C5P7lK+7DMZ> z{{RdAwVU{R_>1(f)`!!dXYO%WTC&8@WcJY!5~WT)t$AANIMT{8ez*Eb{a^G?efnfq z4nKR1gDXd*?|da~PlF{(1ILFN+Bn4j0Hu@lN6nYC{{T?`0H9yEPhU#+r`msX^ZxAn z7twu9`u$EGh3b6nAG5yd`(b#*0mql4d)tTY{!=HNACd3wV;m$vAAS3Ui`-%M{`cFs z%ZJ_f3}R6UTUy51)b%}aAO1}L0L)M6pZsonbCc+v;pa>4kLoMcJuCO#&ie=Le^iXUU?(P9a6NbK zC#|F}KexWqa`S1|kt+I!Id*SM^!{Y!$+sEUeY?*+xR3AY8NI;66ZtRg>#T3vuhJ*^ zJO2PZ-tXdmp+8n%bUjP#cj<1NwDk|S-kV=Z^*DP@zMA#6CH6P$f&Ty>Tz^83ruvuMAJq@2`p4}0xnAR+zrNmZ zc`eLz&3RluPEWM-;_+lESjJVKBSh3$`2-q2SD)ig)n#rl;1M5cKmM=RHy^`(!nNvU z-|n&h0O~z)m->MJ02RNlU-0egeDBrw`1Jer?=QGNZat_6t9`)qE*Gc%%ibQ3?N3dW zZ_01EpKl>s`ZwMG0JqSHRmi*VTCai}bJJe`orlKG)x4k8SQ3 zVy(fi>}TXD{7U)$_E8?Vk9+zrx%Lpf$F<2hu^%toWBiYd$N7Fgx9{t+C-|HH054x? zKgnO)$@(WK`fuCc)eo|L1B=c3r}mMq>BRLI&a1)X`}6IcdA54`=dx|eZaKUQSDWZ} z^*Hc(?`hM8>OP~&=l1Ii#-`ZU#Qy;F{{Zv<0NBfeJZ6vD&&+z}E9oAzdY@C&_191Q z-v0oaZa?Tt(P8>}{WW^0qWgpOf9~jTIh^ieaeX)3-jnt-+dR5)c|31Z_aCBima+9O zefqZ!iaj6FITCc27m@z}i}U6CcJ$sis_XXN->J?208IY??e&w~dybq?e^Gzz_3sby zE&ggg&3}=vZ1BF{`^5S$+HcjLq550f+}~^I%XoShzahYoes{aR(Q^GulS+EGrWuB} zE79v(`md!0uKxhO;6;5>!Jpf#Gcz2q{{VmfpQ6F;4p`L9ANKzM>3Zh#>p!hNr_}X5 zPpRs9^k>{3)Q{P3wLf;g%l(l1i|@Zk_J7Uy73)b-bM?q~f+U!)(jKdygyewRJx>3*@u`!nq0!1jl%{-yn^T6>$&y%X-Q3z_N? z_m`%6hqS#biC$-``wM_u)A%pyzLici^GwtWz4`zBF+;6pB_x7*cUtxQjig~_)?+;~q_on-o+n%q-T2eKB z7pr@9+5lk3e()4sl~6P zC9QgTm!_?GjyQCUc&hA}7A$!3B^rPE{{E0NoS7atj_21YezWQ0pT%&Xe&RW#4nLp# zvi^Q{0DtR1`ryA<{XRYa0JNAV{*jAlfAPou*#7|AuUx0;{_1_Ue*XZ{$=QGO{{Zg) z0Pt_ut;om0`txatiPZY-ey7y+J#?7;FIWEnJ3sn}KU#jZ#WE81&#iu;xZIJ%Va|^J z-}eW$e%x4cEhj_6qI2k`;1Nh#Z^zhGb`}2ElzTt+PwjBzsh@S5+a7;w44J+)J6alp z{{U@LG?V`TXK|B1@z=b|g-KrD2f3a;Q&a|_c-ikA$n zH#8GBYEUY*2u2E%VWO30^7!H9a4K>vxbi@H<3GBbO(*7c6la^)EzLfL_gH<#Xv|>r z`Cc5c28KSB9cL9&rDJYm5j*9p9%CLbCJBo}5V zew~*exfu*(xygzWYrMpYJ`*MzI$xgJ3i81_&J8VI76q2DB zQq#Qh7H`}n$*qcF;;8(7fSm#pYDKINOoUZqvo?X1A6D3f4o|7y49#PL?7F47*?1+EgMbL21`*q;w5s4O5p7>WpT(^IjhYrJ9TRfPZ)}zqrXdq zmmfTI{{WK>(EZVgE2eR%<7i8! z4I{nzs?71lRWjnz=6f7#6n33Nmo-C1R8q~!Zh~lKDU?y+@+eDzHPDB3!`CKU1kfbF>)rkEV{}?81_`-@iRU*n2YEg)TJp-g=kkP zgBI1Rr`6B&6ZWv8DI5H1FC4EYTXcjVN5ZU8%c!xSGg^u9>gA1H$ay87y0TpvgHy35 z;>w;h!Y=FsyCYKNOt`U60E<*(H*HMillf&deWxLkW@K`aGZ&e#RQ@6(N!F8Q+2b}7 zdD&D7y~zP=8D{Pf0s`gNBXl>97F5|$td#zv`5?Vi6r7pe3d=S9r_a{$Q;>|teXcVR zBosTwl|&RpwE@f-ifGrDYv*K(NGP_=d=zo4_E!&VwI7UuIP`T=7(R4egFc&mOm8OD zU?Gl7$&D|od}iDpL?*JjwpG6k0k2t-D}*V>jcOt*7K3X(#eP;lpEX<8PthwwOAazs z1RIb)9u-$bX*#*W7Y2M%KxvW#GOLCILPIR&#xncK%;cHgp=PC2 zk=Ct}rEpP~7fSI6;@Og^R8m?6{#J9o{1sO)lA9c0Q8^S5n-SSFxZF>Tk%1bN74>b9;xAaPqXfK_DLeq8A$^@ip?&Ua+aP14Ym+E?y0}dZN5% zOOU&*joCmdqy00Tn_O}3PBD+Cx-A40L8JM$yp+rjDXf3JQ!FDFtVi6S@$~W=gF&;c zd|Q;or4N~&(&bgbQpaqRogsA{qOfP9ko1g(E^BT&y0!*#%DF~%M;|K0<}&hG7CTiq zjZ8yx8^r?}RY)|?IpYN?SK%3++Hr&v7#dtnP($gb5DQZh@ z#O{DCI%7AqQVdA}-{RdeHQHRM4O;#P`xe*+xW z=%dq)oqGds>CERxyD}<)&{ZmEv^A`oFRPcR2U{7bo*N8>Ws2tfzRgV(1!eULW&xagu8vIOMP8Ag0xi6nY|Rs(&aJwaMPka+o*-k! z?UM{sMq_hRF|WBiR9@0|sHruI9TAS$@xk{TiIQV{n~@F~otV%voF&KM?d#JiDOPd) zz|lEW0-|9$=7X)UE~i-`K-*zdyYbMgoDI{F%p;H)IN~&1nTh4U@29ESojw@D%Lc+s zdPYgi7DCL_tyy03Z4P3(vUW35^GO|-)!QdA$Y);K?-Y|KRu(r?cfQk%NeH1!C~chy zpmqaRR2a)K9bB)dl;!1W`JEa1ksJ)3QA_Rs5|kM9l47!fX23`ADY!{Hnz z2Q$7&^IOv41<11*t5&Hie#o*d;- ziKiGz@T>)Hc#W2=$F?pg3GbUK)*|;gapdWN7vVC8J<3O8{E|yTbc}?(DXRIe6;vv) zD2-sutp5PGvTVA|n6s+^uE6G>)z|xxP*TovI_5Cnfz`=^itz^3qT|Hb=>nl`W62+1 z!0DO9b?zsK)$Jo_B`z2#R}$pm?tFsXS2J>jh%Slm}c>pV8^(YXw0ihn_k4R9R@IWr-{w!Btsr zCRoQBGiO_m7Dz2KWXhGt6)B=(v^UzJNZ<1Fg1b=(Evf90IV#DwI)hxUvYO?Zceg27 zuf?d-5at~63E*7=Im@A^l^CveQ&o1d@% z9LgxyCk2+Uazv?tJGV4<%S|MFHOxcCwq}~LDy<~n(vtE&vkN*Dy=0|`Vk zKl>E>cxart2_nq)B`cdn&y*cjK~BXv;RY90U4E*^l*dOQiJP{oZptg=xrGyc%Jpzy z_X5MzJrj^8dJ_o|Ms-}%kSAW#D*0_QKz5lQjH2j>)B2rE0}wdz^in*W_cN-f0ho5> z-5VPkPmZQnjwT;R7#Rkd{49VoJ&$n;^g?bYy`~{EAVV|BniL86+EJSh!-`su8VWHdAGyfwvu~Q}emQHMi`)dUBN{s8HyF8sI(n>0zZ_eQoM7OEpMD+$B!AbZ>cb%;$(ciHt zV&s0{_UdXKsN@Dk4ZqHG`itBppC&o#s8U@-l6<06#~%vKJ{IP%?_2uRG3NUD4n9jJ z>ojod7nPe-^rW5|4#kX~u=E9weu`RMaX_ zm4Y>zF6K3Ne%Q#bAW&}DC6}y{QJoc8)T*4Zypb95^(Q3YSrD&;m%4ZnYVf73qIs)W zLYWQjR4kE>a%4<}THKl5%qTE>DWZ3THT^0pwv4qK_PFzYj z>a$Hs93SG&G({|0ixyugi7v|GmD((blOlX(oFn-OLUvXxJifEB)jdM2*Yy+Q0Qo87 zvwO=#awZE%$m9nsdz{M&CR7;^kxTM1$@8v2rV*y+$9xN<%2z{xX7aq>4K^W>A}nP& z+p2?kEpz;p8ioG=$e&KbmkgXIB*dI9=EbCD#%9-$YK%KqyDNJ5+GpD2_Q7SwmN~|P zvxV7Lr_Ql`%bz7c;!*ItLHtUJr!8c%@`9y*ZWuVwI>;~;c1Gi-I{+gMU-#W}GR`iR zOA2Kgk>kh8Z+cMPZB&AURq`}TT;tVNX}x4kmv=R&I z$|DHc%>|__g16PDF+>W?z-r7KFZxq8Y!jC^xx;3^?Er+caUyCGv&|_esETP zNVIg*l2NJ8jTRySQw+=Fxhf)Q5mAjU69Q7nF{45ba_bb&xRdS5n#2x3_9w^iola>rS(+=m0K)?7;~j)|r+$^0G6 zs;Wg$kgXCo18gBas9pJ z-K%WXT_&N57cW9~3f>_fUl7dMLF{r8@!Ci$M$!Ex2W&3XiN_pAd3;H74>a_S%w~*I zoOcA*LIp9dG$778!Pge2FP-kRLpN!{jmfjhhHDjy#%LLA2qoOW{$x@JR@+rVpn?*y z9N11Q&~Y`{d|1|@?$gU_b*WN~*2^ig3{yHmD^!b3-+a?KAjBXBo9@+OOr7!G{S>ae zQ1LWb>qa3RrUGOxQcG4YlUCVq#E{6@{{YAr88Ar2)Ns`Y%t}!ejY6(SCS&gr20tSZ zRC+ib*WF85_^e7nw3wPlb1`p{qHI)gQ86(Gb&g8ZV~%E&TEdHmkb#u+S)esHWK$;D z7g9sF_4sj@KceDtJok(z9%Bo;ti)dJ8h&6nUGXU}5&*IcrI9=cXPfeH+6m6C7zVnWkk&vb)cnm>)IE3+SWSl zk`EgX3v<*oXVAo{L}A`M)mjqKuiZwCV9V}wehe7??qGDE#QH`Z@i6J}G_2akNwxzO z+H9nzqe;F(s+gWt8L@X+F~Fpi-6~8y&C~w?E8@51R+*Ad5s9=E(_Fj4QQ9UTZelB4 zl^#w!z7C-#WhYldWv1#9A?m8fp+cw=A+VCpJaulrBES+ZW1cxU@k-j4m2x$?)LL)L zB+$)iyU>p#GaKvWGm#V~I8N?+Pm;0ul`y45@V>ib*5$G|_eV5VpcS|7iXo;bHcS~4 zP`~q4*nbSC$O16j9Gzo0-9=#TOeD1p?xUH+L6luel8Z5t00kLxVVqe0I2Z*b8#+d1 z)b}T{d;8zO&!+V$daEz(QhOKC3A(NL^9w%jm3aqA_#&V5#>jua(@sDzm4OV-y{zDw zpP65>5YbsC#M_cWK0+iDF^wf0AbuU~G5-KPq``Yrxk`64KS~4<-A=p|Ex3S@MoztW z^0We$tc={Ksp?L(AP_7EIsw(teZ78m+yLU)UlOK)$0OE(w>SG1TNGv#v zq`qgm^6?ID)QK+F@z7U-M2uRD5@vDawCbIa+*!f4w*yt3nVvgxLlj^aq|w3szgn_V zbivo)VzgtlDmY}v2}VXI zRGEvFzRH%Zw>p1%OmTgpNpFd?tKtlpXyneN4Qdv&`xg1 zPsZ)o#(yS7@rq?A0V>`UZT=g+b4Hc%<>hYRkSwR?%fyFce)0_mO zUZy^t`)v3O9D>DR}gG;>ZlTuzP0Py20}REcP5%Jb!R5TEqf@e1 zcH_`xIG}-u&N)&>j9T_OTR-(%pN`%YvHV`HvDF&V3WIE&kS8F|3RHZw_`nrqRt0rb zR94lP)-gFTm5j2o2MdHqm~qZ{YGvvi!?%{mpU2gclO{ZI7?x#|Tbb18S`_YFa>=>T zjxN_uPZC!<7DgK?KJKJ3q=0$PaW<;})Mk2iu>|A?(3W$naY2Tr&l@H*^=tXh619wo zq=VRUA5v?}(7bq@8sm(ZvDJJTj_Mia-10AY!3nn>}kZnZ#)XKC( zL@uW8XQ>co@^rY&#X?&d+M0)@OEFtA*9=PXfhVcm%c225HbJmmd{n>GGT@nrwro9# zPprqQ4Jhdi~tsVTY$BLGAbn1CukThB~g59C< z+j*-hvnXmHq5_FW>j7WKVxKBDBdvm$V1aKD7$)Zt%@Z7} zkML?WLL)g&lFd8H@>lVc$N0E>jy6hZIVrl+#>#2CzZE)`y? z3Kf#5aW&)yy%K?>d&_FX7GW||Pn@mBK`Y&2#nPO8{^HXTww2TTd+T*{d4K zlw_4Y-IoJHa`k%Y)4X=wj+prnUFXuVFF)8TG!Nv zJH{;l7@o$pIz6_Fyq+?ZIOgE(2BXd8$5y2`>ynkwl)|wH z<-e*IDHxVWe%^h*x=K|m3b^dD%=E(5j2N9ObpHStS1DBF$tfsOT19HKD@~`|t^P8rs)ICBHCKEpBD22i!QYH- z@G>Q)3rYSIII6nrN0TcN#g{fs+ayBV_Tm2kBoYX;g&j}r5!}SZr6$vZP@*x%WXuqL z(*&}~`MIRz7|kc9YET@neZT~SELbf^vz;N zQ;Md<_I&hoBY)p2SXf?_w2L6l=5o+!+Ac0v@)NZaKf>sgiPtkb<{ zW?<%0cBI{v)REk-VDoGMEAn+!Coi#A*HXrPsn=M+89!}LNUB3F%xY~b@tC2G32#%| z9+)Ovl;p+DA#eM*@AH2JuvVy0Q)rJn(3~Eut3LWNl~iW6Ed8h@xt_TQ2jOw9+p3FA zV_4>;C|c5u;+s%-$Bb(8t^q>;SjY=D`_1m-yl6ZEhyjb zMz>f?wJklAPhWdN;mtaj0erB z?CC`mL_B1b49g6PBlcERnzpQYO|0}gkc{+ymt&LxmNH4kGJi4Df}-5JPr=>zUcpQ$ zv{-nx5$GgknP(13&5*|8KLz*H#L293+0;cSJRsFi22hNYCGOd+1Je(qw2a`E4%(_f zhjFzR48WQ){VyI_!g4O0Dx1Ny4n|JaS0!wlf04eq{{TsPyjbTRP0N)e$8U&z`An%Q zH@(O0#}x;{7ox1=J)Npe(N>7dK;_x8EJ$|&;_Vd%V2rA?$^wYWnQCVoRt{|6yqrtN zL5&^8O8IkDe3Ut4`b5UuC^W}unbgkp5y$=rM=g-`OBFVeD|I$^i7yZ&lmgUp%1ceD zM!wthnU1rt{CHv+h-LaOYnWxpwqrbDj#UxK8*zg+B702IG-f5ENqaQu0%6!BYnK_RG8BVrw%OUWx#P|A+f`R!tR^VFDz;89A{Bx+i<1Pq3( zttOh;7LWMTX@{6x*RbJ2vPyfOf}|Tr%!F%d}di$5B1vT54LB3~H9E7R{)L*8c!TfaG$bBoP>Z z%XXe7c565Df({2G#g7q#V~AHpO$bn;iB=;aNPMV*amBB_Q5_DqiX|LxiHBY?gO$jd zrYF1YLE_Y@v?kIL6XVicZZJ)imTN6k)Rm)3Zml3_%+$xiw%N;Ob&nk=WKbVA;uz0Cd*5GGEjA!>M~$!9R9wfQHAboR+l_4rEV}4C=~IxeB#0l{swe zlAy^@I@G$l7P7rnqh%$Fg>gXYe3c6&XUm)xBds2nCMlA~Oc`7`3k>D#Srd znLQ+0opo8cY9!W2w~ktQWQwZIc(Zg04%SqO zU5qZ%D2znpgYG5G^F;Quw`TbFJ3RHwZN|YLBW4(@HJW$sY6mbzCTh!6LxRAAo;-N+ zQAEKlpwPFHA!9)1#nLz}B$^>xou{r`#-$=9M<~&iHFr{w97GA2nbdf}?c0Q_vv_4p z7%;v>SJ8icKT=;}e@kA}x4nN*KX-kL_KVvEIgQ2S``6YX>Aa6xyyTaD54U~3fw{io z^^r!Q9B)kH^6FB0$~t(yi1yh%>Q4?S_gV4rI^(0u#%_=TMyRr@zsP*!9Ny#F`+RrE zkvE7gKeo}YoI042>jZDh&Q&_meyzXimHTz=Zht@9U!w1`9Iv_DodwbL6>t5fjc?tbFK9JllEGWxQ zRnTk`?>~n9&OE96QhnAXRmmvI8A)9J4tF*)J<}%l%sM#2ILsp@*4 zQ`Gg$ulRZY04o0M{{S8T0C%PBpL=rsSKJ?^Z@Hek!5?&er2yQ%N3%UsklY_mA9j76 z#$GKeKV9Mae;O<596l+T^_~w3e0STI#Pr*7xS!Sk08AgKU|KQP#|lNNp>a{BS80w* z%i`0>!dB%!O#MZB42ma~9FbIKbu-7EVqzwE_^q9Ny?2!RqyAC9$H&>A9;Oq?cWQ7_WuCWIaI9& zACb!R_=V{RiCM+4bHxuKEYA@GJy4{Qm$-^-oN-xctsF7_#LHfb*&*jE}bilQv9t#g`pl zx3KPMGu2z9ot{wvC0YD%&tv1vW6h5yT+C$Ul4V1HXM}t!pO8;~C^O}>HDA?IszBs0 z+K>Bl*xTlM*rdw|r{yPX{{ROIMFVtfwp@7BC4&+q0!Nb8;aAzJ%`shvc_iH`9SH9m#12U<~H0$FnNEaFIy5KvnKu)iL}p!sH2 zE3f?SV;*OZE=`|v$ddk|Boz@@22|{_(+I^(FO*VrM7-#*r!AwwF{?Y9GWS|ZH#?8U zb;s63M0R=Lnop40oK4`919_DSq{ls8BqI$j2F$<&ootSOm^C;InbzvBNfUXxl%wR% z&h)2&=^`kE&<`f;vD~jX>ice`zR$)mTXwDq@d17jsh@K;~azE;( z4J#2Ji;|a74Ez58PPAs@*uuMYQEtY_b#%g|pRfIu_EsH*{7X3ya&*&CB?7h0L~hKe zgwy(}-N-u;J~t-G$WKujshHXEC$=?Gazw>%Z#vdyXoP5`(H|{zrTGapb+plnOIilI zhq#(3DXN^7@n+pimt^|OmLZ>L8a2rREo78V%{X#dQu?*6ry`yrq!d$3gB)bWM6b@U zr2{O)L3@?<)XWr3TDS4-fs-Jj<_)*ymEQ z%Fyh06Cn13l*+V33KDkWdry86M8d^~ImcaSL|3MJ#ints_LHT?$~uW}Z-LUNDlGU{u6EvHCmYSUsYeo*WUm8_uYr>U!B7#EUbyeLKe1`F5 zRAj75BxFA0j$@8vQ%aq|rl5rdFCC&~(s**1k~rMKSu%ANJ*~$ra1MqH@mV(ZqjPM2 zds7P#vdL7YO@w~*#WCIzn!opOq@b37^y8!6&71)owS#Eg0q_rjt+CB!Hg$&A*>l1)9rB`GP zsiL}qKprdPOIBz&&V57-AOVaSw239Ogb`(g&Ivwe2Nxw&c?zHIT{4Jv|fsrLAy$5 zr$8p3!8@^E{H~#ur+`-~>I6}!Qd6YZFC-t;v_PR8>i+;E>G6tZ8Ox5?=L9VdQadQE z`B#_ZgisWhh@VU2zCmiva90Z4rlnqDa+q!3wp!9?)>S%{0PcfbQ_Y^_L`7PzEOEb! zl$B_CY)n=J#DOj-_5{x=$nqXDx)b!GA|sT>sRddlF`t*@XL2`lRn((#!@Su2U&^c@O14m`&)%3SiB z#0j^0#B!T1=V?KcxT8@t(TAl`Z8?q=FcK7wRaN7SU8l$-Y>IZGlDJ)&u&@5fIMW#> zazsIjxRxR%d|aEWPb9YXp9>*mXF8l{75u@+3REuq9;p%1B4jzOV!iwik85YPm!XRX zeTmjYO1_-MxDQIm%td+gH4o!svUEyeO6pz5?M1!#$$)f=?rO}QN|Hd zXh}rUb-MLEN5%x^)Z*#b*-quTJW?q~B_eDxh_ci-33@LkNCi>2fO;w18!j&8X6UM} z!DhH(3cDaCUJMlQcFB_#T${vWu+}B`1;W!}B(ij*qXxIL^YW9929+byz*-^75@x`)Z7HoT_JiMk3k0Pf^B~2~i50p&GY7*S~jJH)oYaj~5 z{^%_gf}wJYW<~&==>SGUm7prW45DM1P<^(^K2$n^U+ycS%n<%T7rZ#{iIOiCYRFu2 zMYL-YRVdM(Vl-l7y@_RW?~baa1jMA*g9?VMN1>kocHddCX^#}zatJRmq&{~?c+(avbA{yYK83g6)MBacJ+P^2X6J~5}t4yhx ztk-+gi1b7%qc+8rP*!M^u~lS4`CM8m$nKS502#t#EEuQOFP>5V07Y4?8%V^~bXw!6 zel|*GJB!guJEt>|5z~xV@Te_znAG!UgwaQ6gq8LhlyS_It0<@rJNtL^3Z_;QTO~qLQMi0f6iH}0(a@-piN-lnLOLh)bwYx&!DFa#!8Jy+ z$wetEXPhZJpB~|Iw;0MpHdOtzv5ql>QV2iBd}ro2Jy~(Hxf|7CKElY757FVrlLhWQ zEUc*0Q5T&g^`9dRX8BZ9GYp|qcQ)31xz0@HE~M{wk2^--nhJeh1$|kyrg=bsIX{|M z@sn(75t9OI%(#S6lgGZ&{NihUv1qJh&RM%)7g>Qm<~s4%W_|@MSFX*Y1Orpyoe$P* znPzfi!R_XH=|NDcyGk+p$t7^N$=e1uSw!*ZmlMY_q_-M2x{2i*UR%;-B)e8-)9Brn zYLn8>N29SbC-M+qkQHcnxa2=})qOS8-(+Xd0xJlSjJ1my!!c>Pq(-Sct}DG%gH<$j zp=!d@naD{f>V7N6DK(T$knD$_E2H+yFmuf;M^!*Bf@Ne(ccp^Ta{a?vY-N*6C|BM{ zY?`{Sht*}vxhlOvk{nwQrNnX-px%VDl-gf-aHmonQOE8#3UQp%X~|y8k+o>_ty8*H zRu7IwPok5$fSTJ$s%-_51+*r!n~A{at?)SDfK`{R{17=%toY7ik{#eHy ztQEIgXxdJ=@^{RYrThydc=5NJ9!jWnGxYV7l*~9sotWS6i+1JOA_6MVn3o~Mr8VQO zsL{)}CN!i0Lq&2KFV;ts=VO(0RZ!lpSjA|})MS!t4rxYvqRJCy45$@IuGNJWiw1M7 z(?8U@)3JiX2UoRVt{V6?Q64g`fu$(81zC&Hl4&69g#@}C_oSFJHztf*8#A*eztdGz z<;gRX>SO8Wi={S1xR^%7D6EWdm8bsG;*7_cWyy_UP>D)cBl&ZV*B=sgc8y>=cN|8D ztY%R0gH@T_GE%~Ae3`YWNl=j6s~yUL{{Rn$XKI8oPE(U6L63m#NKz&{gWY1`^NWn= zDs>f(s63`~CS#|BaotW>228%!GsBUcT_xELCKa|6q(YN`$CM`E?BSk%J1Y3r1&G&Zj(pA2l1jC= zy_)C)q$G-_II66?Pn%~XSiz+7#v7Tv;m2SnQ!%-Yalt(#g){v0VuD()?cA;&VHwn` z&#}1SEl(BQinLo#GuJf*&OkrSbt=27JJ8i~HJ|&VDv-Rc9nCHzas}s;%^`)^VV$@yu3T zs~=NkOB;c*R#YDyPnEeyZau$JV4LIF-EvaJ#PC|Ym1oA<_(m92Mq62z!&6|1+UPfe z+=YV>zswfzj09F9-|`@^rG?D&ar<+IaLeN#Q^TIC_iS>lq+W$6l>yx2E)tii#V@hv z{jY1SKNmcuYf^YPMR?h*6RqU6teJvoM2uBUI{3-5#;-}9`sw1Uk;Xt+1o3p`=~n~d zPA>2#spHCjV0VF7OJJG5K4fgkn*&(x>6C)QI(@!h!1+UxT7kJ-cRZgtGCvg(oAzA? zX*>S_6aH$)nxRzDT7ykS%9>|Z37r+28n!v=WHOqS;|i%r8b)T0le6(DKHEz+qj?Ha zr`zn)dalvwDH1tq)tU2)(Ek9tNH zG@SWAxnAEDWlq}9ui?~XNmsnXFj zY$;YR@KwP2JfF-QrOBLwCXQZSGj7Y5df0HEE=IIgC|)@^Q^7XEW4!U?VE$G#Lfm<7 z!X!t2UbEJuwGhQhP|TRtEvh2fIwLrEv)8h!NpMqlLH^2!oMRs1F}TEub@8tJgN)p! zAo6p*@^@Qa6y70*RkEr{n6Es4iH*&VXt4$q7ibhb;&zXY*9VZb`BV=$&Ad&Xh^;`z z+R>1zWH!V4yT6OGGP380IL0#vC%90Oq{f!xaG*;Qx#<#UjEU8(D-vofMlO)2G zxVF2la!fxtYSAZMh7oy)NmLo@r}_?(S16%u=8xSeqj?}MR*FexVCEyqN+@?HvC$B+ zV^I;Spvk%BbSCjS?GO=}l^$-gXAtTWIQrF7BYbMSiz+Iqq?E%@%w2quil3~R3Y=LJGlxDsB}8J%PN&8)OR%14 zKr$5s)@ER4K?f=3Hh9GZ))L~B&&a#Veif_;rKEWqk>5#d z#F4NR8wW;K1w|H8rB3$EO!!@&8q%!6Q6Q4{JJ~{61a&npV9bLHjZChXnh1hu)ES|1 z$xjK(SviGAW(W4t0Jh4iRL>M+Mn#i981yv$b_~qSn}z;`DMTfV^`MnnA)}>V2+?QC z)RZ|&0TfK$R?tP1^Pyh#g)Hr1Y|`MyWT2T3UPcJkIA+* zc@1r=Wteq_b=YL$l5!2?-eZ#3l4TNc+^z?$?xiE7h0 zu=k>ZuSwMHMK`QwTOs>^SCX=~BMo?t?WV%Nkn#1UR5dpc1(ALiMsFV{beD@JH!5-C z&8T`CQ3T@_nWC8EGSPqDP>Z<*$qW^oS)}2yK08}p2)a$8(&SlvxW{feV%DQYxeZCi zbS|q}Z;)DZ%7Wx|3ojf84224#V`{Nc>Eal(L~kR>4rlxFAtgaGx1}l-d%_5x%hARe zGmsdJi9}XrvZwQ#RQXINJ>?m+4NL+(EUV2HQ@LewCs&#*>UChggZjHWU{rSGdi=J_YjmjgRl zZQVhN(Zf#TAULtj$ud+;!mM}6+jrL^6Mj&n@3&7wEF3PXCs_1?de-fTv?%oKQb0sq zhBN^u@w%{Tl2E#FA5$;2pS-FT=X`}_5>S}Z(L7^&onkJTLnpYLbCF@4!;3_s9J3fm z*0z%;l&P?Z<|3hBWc(pFa- zidQi_`CCX>xP^4pSPndA4oSl%*oZ%gYn{R-6XZ;x0u0(aN00~z(?cTiNUL?NUvM$B z{{R)TuruTdmQ`kB@itb@zX^=-FtbD~FXM6d^Wt}SCGn{pg+}*C)80{^m&RhXSIDiI zd}b~Xw0*-nfa22m*;%yHGHI7towhCch-BH;NbD4H2$4h^t6R&-!;4R0y0(I730XACFIT2Wg#$Pj6#pjC3j0oCQi|q{-N52=_Cy})f2yLs+^9Q z3IsF@L0;B!7=pnPR8&4iX3C1r>#$W;_|7TGj~}(%`EhEF==q{xsJujRm`f~CSb9~U z&n&h$)S)h5Mf*nh@r~5-nTT%Pxmg}GlRMd?CF`S8YSL`E!0an!$>>?*;Ai(4VB|7= zE8WgU5vj~OOd9x?s4wB($v)-Jb7ADrHI=(4q52Wx9f!PyfD}!Tqn!PnNY5xVqg?47y51z&S7LQB?XfkQ=t(N8Zi}!vCF;V zJ+$}Y_G=&sD62|Fjmn6kx^$&OlT%Brc*XX-SQ-$)mqS05Pm}a{W@jcy$3-qa5~(!? z7cUX9V?vu}f43~HSn|PMixuM>cl|t zDb(^sZnrrEKE+V3jSPP@P*;lvS;vMg7;#~pRH@5&hIrBirgjuZmF3HcA9hX_XC5%R zlaf@;x6i!ZZKbJ?6E%3IE{3~8ad+zzi}G63bu-B8LsW397iVAton{H{U>ctrHobw5 z1~m7czeYe>H`!y3NcvtnnXb%m?WiZ3;}neINrWKHjLdcqytp$v?LUW24l}qGP*E(^ zxa#P=lgUb~s6#}^?KaTn>PI3W7a$ads=;3^UgIRf!<8a&<|-(=jrjRMwm#6U1e&={ zLF03)#xe=nUA+qE8d}18g(dqmO%8OGitvcqja2Z@kLs?Qg=oseQvfz z%OG%p+a(EUjv0+-{G>v({DR1L(6=Z>%H_V`!i`a5a~A27IA%LU*PWu_p746MnNb1g zOS||Gf~dPQtdunKmzsrFJw{SRD79af_{(*F@0VkcLPg^1R^wlc&o!We=YN}u zY(i&Xvl;M0O0wpSsA>$Q88!J42&gey3b-~;;%yj}o|Hg?$CEZ_jVXjAN}S3kvW+2L z>d|8|W%2FAkpk(D+gMgL3P6%LVwPrW+IO88h8n7hI4`vd{{Y6j+$q0$D(ZVU z@;XMcW)frGRo%1YmHBC~9b?A>0mzW7#=B`n6M&E(cu-!e-1!o#>q%Atl~bA|t5re# zsbYrAnsF0iTu<-PvB{D)Y%NaGw~%)a+az2N)DN?D%aOC?vJ_8mrZ0I?wT~mw)-&>X3UcC&qWb3Enll9pNS3k*B20uT zYB@|97GM_H`AoVUe8xzy$) z`DGhpgMnKU=l^!S6eOv^m9uIW(S1?|Lp4><7$eo07^(AWG+8R@-@!y)ao3JSQLWW}W4GV3pXvxB`o0t+ zQwn{=N}j@RsS}~u4P%WV5cWWq#3Eq~kyAC~uH1R`&Z}S}H#K`2snC#fnM{GN!|XB^ zZz7JtitF3vg!HV3jL0Kot)A6~GrCU{Z|2eT*OME9qZ&}VaB2%ym<6_ZN#5n;<1L=) zS{~99LupJ8z}!Z9o-!>MRR|_u@+#VZ3a~pO4+dpyQzCKX>YGZYNP#bL#siT14(rKY zB4}TAC(`lB>I_-r(jD|M-fGA*DPI8-lPjd+W05qjeKZJ#x@Tz-aCY?BWZlytpN0CL_(c^_@D+$0Ab{{Un z%4Q4$-csk}F}dX$mFTA?Rr2hYQr1r?Dt7VNqq91fa!JrDU%7Vc^0K6 z6!8u9Zq3BpgbiM|XK9oyCeBMP+V8i*s2o7!cuC1W%o40jYM-*?)y$OX%s@XGNw$-V z7)BV(XRV^3R2T{+ru2U1FDBBfX)!6qeO;Meu4n?rQJ!q%n|7jECgO6`Dk3#9f@)W+ zHmkcMyC68Q97AX4T;*yzA*;J`{`}3IMTjmJl;)L{W62oQszmFJZDl1T-P&J!E?Ncb z*b~mQtY$e_vH%G|_Pzw;+FWU@nUod_Wg)gS{lZM@b@3|S!#3HldH0ol$wXMm$!(tQ)6l#@ zw5F9(B+EHxxQ93hZIsm+Ni&b9ZDFF#5d=x?9A$ZFzb=$SLZXnnTBp+TVbrW=9Yd2R zM~i&ig=H|HC>>Zcsi_moW;0r!`jB7t&19%1VS|v_)Vnj)UMj3PBLwHk#;LmNG2_RI zaMY$0@sn^LG=2z0N``&{8qtYRwduHFbPz`-Vnv1T+`iz*VFw-((zQ{IOwj3Hw&?`D z-MLemyDqCh^Hw|Pl>$K)l&TCJNmUw_b(hsfaAN@p95;tjml%lD#PW@NUn;hbelSME z1j2}nzSdSHa}Hf(qp9x;<|Yg-r6OpiB$%>C$aZ$9V#x6d8?a0fIrao-rBwHF7q)>| zl_rM)F_kLCwv78l84n{8t@*$@aGw)vSuQFklFL1cV-Pl(H=K)P+gDC2k_~GU-|r!8 z7>L<}=q!wD8p)?^!L2T{6JTQ=>MZnW+`ZAL;DK-@pXwQ0!#Y+_0 zNDXGCFFN))0JFFJ{{Sm1CF08&pzK%MkzUJnr1G>`4RommA)>WOAW4MdNX8YPX9gI_ zwNjTC_X^&6>FB9HjbO|f$aghGE@cWO45aHNV>hW(tp`ae8Ijpr4&YgQ-PxDwuBQ#o zDJy!|@3kPyM2S`;*N=}CzSDT*apC^}qYXL9mkbg4M9h?WbHze{bR%DAzYtFSII~%r zt|dlSKlc=E7ZacYCV_A&UjWQ}pQ;!h34ao%F33I70f z8{x$x=nP{WF(!7n)aaqX@`DSin2(y@K^bU)xl9M0jE>3@!f-)Uu?o~Rnxgh>UCI#V zn*=GVVI34_s4#$$$tgwKh@hFSFEoXG-C)7Ta`?e#D^@{@*xNR$vyI}D@xiE_Ou*%+ zRgKJX?kw4n$efOyR?9rq60%efvJ@gF#a`&kM$2pp&s~>YoU!!;tXVQmr5I^Qnxr(M z-_;{0f-zXLis;6Q)L_GpA;hDi`%8PZ>1DjIRcot4l-Z-a`B@*D zmJ$8&DDLz=Y(=l+M)IL4}UiMBAp&soGzVJS`8?@ZigvA5$*< zyjm!6+IYeFOO{BA%37b?EzE6NqD(QgauchL-Bc{|SW0v6PSkuN9Bkpx6$4|dfLAlG zsn&wB74bKSQszZXyQRlUY^^n@W@ppA#Jrqw`=6$Jkyx9`eS%PWig@MW-ep_IN@6y~ zOueg>4H65sgCItLavoc}mypxf3qKd_q zj%?b=o@Xu?FuAVSrPeHrBHl7$>yk6uh=|9zZs4Kv|dq2|cuUDL9v!*c2%>-M-@q{^z)!iWlIir3y1*2uAe-gO6(c=A(J1eou}W`&jFX^urL zQYfuoKg8XrS!G8m2nol1@XFF#$CA#T@9G(cmLZA>G0NL4+J+t;WzD8F5vg9Gc<}-x zte81XKq3L`RO%G=s@)ciRoQ$|?O3_8(Q2&_AOK9%;X6o>tMdV#v zTUjF5?Pf@E5kHfH+)K?Y?xj|LYpR5--Hq!JG@Rgp%CA%zSYl=7$QyGp%H2T+MjGy2 zk;xdvJI9JsNj_#|)+3qcx?LDQmn#XOh@VY4RPmiFcz8-N%=WhuV}`7e;pYQVADJ+n zIJJ;jkDf^g_hK8Z>YZURKFivB9f>XEg+Gkyb4r8zO~im8=$6a>(&3LDe#6AvBs40HZ5A z`C~1h=UY7tk&?*Ic8nuO)iRO#kSwhSsonrEHi{cU$?aq)HmTI6u~&}~xfPu&^3;N) zA?7DBnO0=pfZCSi(Kw+007}D>w5;b|Lo)%lnA8+?a0kjVtj=tOK>A>yEmWoC*yfbL zi(7mws}MGHQx~X(7~Zx6?Ub1V^R4j=M`wgYJ~Oy+Ux{s~4=l1f*_!$)H(skcOq!cd zk;iAJh_;{{6%YMfH`0bvgr%E7Y3`HzmitU)gB>XnR3YU^Ux@U1R8Ws`uWnJ)p zzX;cOf!##MaS$G-Np@72`OvsoF+p1)R)mfOMQp_9arDMU<_X`@P zNiN&x{jC!!MKv!;S+G>r3sWo&C^pm`9a31($h1+#+02D8-hLuE3||w~FZ_%9f8W32 z*Zi3Moc{nM-(@`t-}-O<;(M*VL*G7)#v&e<>VEFzaCu&j_ix!=sw_L2HC{Xg}S`u_m8$6sms#H97yk8}R?@AB#&tXJ+&%DY|I{hRuu`vdP^ z)Bgao{{U26Kf1oj^gppa_x7iwd&APc;`__ozKQK0ZScJ_*3xSpH?R7)uJhx@^e!wajbn0Fpe}7y>{{WD0^Yi*!{VRJH zjmP~t`nR?J09OA1dHu;gYI+Z${lE7leNXMbv}ebE+y4OLzi_?l=!VJF-`IHGm+8%3 zcQkR^w%(h@ta-1i7qQQhd6~v%{X6>4m)>F#!A3>@02uYw*VBJqU*;F};p+U~c3=E~ zexLsUNnY1BIES#7E}fAO5S?wEeyJZ~XQCpLyJG zUi(w}=lehUx%94RpD&x~UfKOj{-6HRZ!i?9Xcbwe=6Q{+;V4+zxlVJ-zIIv_EUh@Sw+o&gSv`@$dftS+#vW9gw#o ze6K~UxpoXsr#(^SCRR_?zlZ(5C+`ZA_{G})0K#Mc0Efr&^uz4`09eh~`*|___@Djh zpZ*W+>+0{&7yUhc-1ZN=hw2~vg!{vT`j-3G>K>+j&;J0N4=2<;U)_IiIDX>hQ1f{{ zx%U^`9_W(ue(w92@~O3D%apw-Y_|QvdeXdaF0J~%@pfNuSGN5y&$YqvFU6z&n(#mC z<^KRL-<$nE^Z|+Wz3PnlC-D3u{%d*9{_p&Kc4>@b#*BFUc*~E+U@}?tOMIP5Pdvp(?Jdj;hGOt1AU%WpG0)DF-Wq@;~3|dY-zd z{{WG%@Q3$L*xz?Jf34r~PwEx>WA|g*zNnt+^?$P-%=Jz;vbh{zT#8@+01@{$*{@Nr zB0L@-<-yrKN7}rzA5Z2>k;<11-dvURYWLsJ{{V`~#(kIS%24q+mo7isFXjILf0F&* zyVqC!Q}i`2`JdCw75QU4f9)S{`@i^qZ?9KRet&I;G~+zo$Q?@W5cv;Dm4oa2P$exjkQL z?hZ_A@+hijvz56GfNZ1LtyKGWJB zue|p}5Av81{{XzXf7$AWZ*lHsp4Z#TU+EI3{{Uoq`n&v5e}zAMeXIMSU)JyW1ofEx z%Kc4x$Ek9C)9qhk`tPczzYno})#?^bRsQ39{{Y>bo<|}+>ieX(6#b0&)Z}n|82Gu3AH>Jq<6gt`-Q)JZ4fxAHu%D7Yb-S;P6KnQQ(tku)8TX#|OF!Z~ ztNuxS=l1=NmzR%){{SkV;OCp~4|4NAS%2fj`G0GFQQqA3bMN0@`)%ycRexv4-k#^- zb7ks@?{BqUufgSUN3j0r&z6?t(o(c|eyep$LAXAt-A__4(0__21RrDin!mQ5!zA~S z{*U($^74=JJ~jHU=#iFJy7niho5=(4zsUapW&Z%Of6;k+WBf&bo^ChZ&sy}~(f8}? zA8NlCp2eRcdk-^ZELL{S|wqe@MUOtL=B(n);Wre{JZzKfB}7d%o=Y zCjo6vHwTsX3(iW6;vtHlb_mQ4V(lk8P+IO};*}ta%5Jlj~W@ zjrgcuzgzd8^-cP1{X6VA>$?|XV*Dp>VjOZ!*oS4eWbgYARB7ykgny~*nbh0KQw_b$9| zM~@u(m32luKF=EWKKCKVj~s4y;(zA-%l4T0U*E=Ytl2%ok8kZvR98cFSY##+T-@9d)S4WCz2~v{jeAR01xi|+CI42_b>SP{eFLpPksB7-(TZb z_08;0efvV*rCv{{djpC0XYI$d{o%-$7SUL7e&Btf`-$r>?Js|G_`FlloVh(Y)Es*l zdT#@{c;~WvFXA6>?|~lw0NCaBxi#}f#Qy-oNwNO`wCX={^7X^}pVD)CSxee`p`3qf zn1A+^t3UUD@cR7^tp5O;PxH6#{{V0De#v@o>C@TXf$v{mf%cah?%%dP!u#)w?!GUm z`hG8Q%X>u)W-s?YBNgfB*pKXG1SuZu~ zus7CUTD^Mj*Z5UAKCuL_TR31 z%iX@Q$zeV>r2B8%KAFn(uUhpELz9J+y+4P@b~*T2lm7q{^`G>gV82lJxbWBMncM#W zhwc9WnS4k7FSn-q{{V9y*W2XD`Ak+n=Klcie7$s={{RCVKUn+S`l$VKez3jA!+t&e zceftC*7P&TT%x%3DD2*9hp7Jm@=1QCF#eJJ zFZw1vPdB)JOH6OCza-FcFjZ~TbwPp^`&4CIW25({W73aGrYSZ%zQF*pPICw-^tR*3V6gKVqr7Vw zl|RgMuyUEV4W9HTb9)K7Tt%PfU!FHF2HVy5%qcznh51ZcR?Y z+@1*QP4Md)#T%^2VC;$KF)jH!aWy8_J+JAev^xXDVL&Wnb{v*k6WG*6R-b`j@zHe| zl^HhK!~I@Nh~ilWZnR`jWr}v5awlhx1td%-qb6i=@?KMbWSoqgIJL`83Zix8j-2F^ zH-@~a+A9V!yAoGY7j;Wj9hvIWUPavzQC~?@R_w@NGo~5${Hl2j?jXYcR>2N2se&VK z!d@=p6>Tx*>rl);7q!Wb6N72TDMj}n&rf14`Bzddko>2x)JC325+P+kqLAcj(@i^P zRb|6WDN;u)D`zV;>|(fFeP6c`o>o*%t&Q|jN0$yOsVXT!J2h{yCdiE|3t2Lx@I%d! z9_e04?P;;B_mY%b_G)W0X<>^Y)VoN!rh#cW{84rp0kuWRWkWlp z;hoIi_E8ou{VBEkLyhZ+>TcY<1+sEX!i`tQy`SQun<*4>PP9bmzV2XQLplYaCb@J`|!kJhwb}p zM2$p7q9>Fcqj}l;#3da$Szg&_uDpAol78kgk=BVOAAO3|#pnJ462pPsRnZFg05 zjQO%9pFX|DWiVnYP`5P;mNPD5ZNXL&-nxf^$Bs3V9!Ff6NMa+f$@zQ_4J%JN#p085 z(@u2`F@Ga6!uu^Mqqe5%Y&!BCyO0!C&JYnDyjvTH{~hCw=Qm z_R^bKU-saNvpBfOxOb#2}>oTTH$gyb1BVz$icZOlX%)`a$2K$dI~U6zefTHSB? z5!0?|Ydnne9>rEy-M$Mso4a;JhS^)kiy<8>LPrXv4H@l2%EA;>4MTLlh)Xspg$2o; zpKjM4c5?#KqinV!Dq$YQ=&3>>S9(yWiX()rAc|1JR1_7Up%ZBc8JE#Xb@Oy(Kt7{c z0LL#N_Y;(&rgG5-EfOJ2Hax#HvRkl}qR`xMFwask*D4`vHH*C+wXO4yFX}|cFAE_w zXRQ%=E{Mvmymm=dWE>h(Qd^GMUYgOBvW>esotI71E*k&7jgO8jdpj~LODytc$P zS5%RVE>eU@IU^iE!4rC{3)q#FQgX-Crb*Pk1VgmmZ70=US4tYJlVCHD0oFjziqBFuSN`YhlAv%1-Tax>IgJzTXwW z+WMH|i*@DRkrcFXQesdYp&rteo3Z5x#w$PXFIXm%$`ysf>5NY+U^x2G9BX;KCD+EFUXMUrN3lGoy-=Q(qm_-9vVl$T_Tfl!*bTqA0YA)Amnl2eDbI6{4w36>3bY z<#c6Isx@T?IhIJrCmc|v$e{)zEQJj=4p~ zMng!bzOcbeYQUn3vT=-#r?emG-XyGrCY(^YeDr){#_@hn(+bCq>ltTtVLBw61qpLX z$HvF4cG=a58!IPn`=)AgJhRe?>SE1uB@r6pBFltIXMvs7nyQ{omyE1=xNE7J6)t13 z^NUI1xzPNkVb(N>y~@yG4>&87}fcc_D09pCp-s91)zPoljE9$3jbXO?uxG6b$r?_}qz1-em`BO-5)x zzsM`G`W$$0;<6jfS$C;(T4S4rA^GgMlx0e+SfBG*H|Ry83dyAPJ7m*9fc`1oSml8t1lBkoppA3w)8VAyN52#vjU2t*s)GklxFKeI1x7A zm0j`}rDx`8xg^N2%M$zv;1Sh`LSJBXpERSV#NBx=Aa^yTuA)$T5Xc*$=QmI$s@#=2 zO`fJyQ$+s&of@hL%dA-uz7^S6;zzjaB(0>`P!NPiPW9CMV*H}ow~DI-mP|5p9KPf; zRbZ;=mS#-%#2BBj?+MTKTqrD+OgqkpxG?>^-&o3bJ+2tklPu?{LoCQUDPh#dOsqg4CBv+B+26gi%5AwX4?I` zf|MHKD$!+O;>A~v(M+11%@uZL4_3H<A7};=>v?|AlSdS#MS8g5SP`N1~jxtJ&9^!H2!zY?2YLtw( zmB+n2ax!&Bs{|LZX+N-TZX~vvFBO%Q!`olPkxG<=z~< zN_-5FsQt&o#PjOtqQl>cjajn@>tuNLBk~oPnOv`F(GkO>OiycFsiZm33mmQWEyY&IF=u--K@Zu6dks>}3<(CF6 zS1S52kbe|1Rcd%DL{cj2QDJPHxcUJ6hfBpP$8%}`Rf^Ii@*SVGs|}gE8gK*UR2nY3 zzmkmoJjXUnbB%j?zTNT-F5&`goh0n7%-+c;qjrge+RG87${oA}6iI2C(=SmD_Etqe znWauV+SZFVoVz+$(Lz}*widQY(ZX6_ALV!Mzf1=5jFxvvh-37L+S>3b@h_RnG zoXA_Al#wycfyIm?jw2o;!V`p7CZ-00>BX%zkArPe4AK`jMM_y^MQ3-X8uOxR@}a4S zEY8H=WIHJ?$E^?i*9jRRyl}EYoH7eGn?=aKdb>hXS<{NkMiH`uY6e`V+xv`%S*OP7 zn3;%LHQ`x^IE^Sw#}D$+6pW#O?X@$A| zjxltm;Kvq^d`MGq2PH>!LP3GMGMT3;gOFj2h^eRHvGTlBo!%hHJHUPoIQNnjMbk(E(ojZ?;87&TJbyBxk5XsnI_ za-LF8x3E6pL>_CEiHMbHt( zkV#d7d9h>1kz~g`Ih`FtM!}8z=qQ~h*iBc|qBXW?rnIq>IPYH zFrxdT;olRPg%wrkX*ZabRMK>~Nt!2YsMC-#6Q?SJL>a|2X7KDr<8Co05hpV7MM*Mv zQbmsWfNHoIB;Pb60tU{`GcMUT4U<+2iZ9oywIM|lAw)uTIwBcW?pjVHBPwzVkcb$3gkLYC z%?;Zm;fW~3%(VAvst7WozszMslW|>AXJqVfL@DEuI$RQ?siIT#LLo&hcoP?(0?aS>Wr$(Ugeng5b{P$B%hk8(uz7069AL* zvXy1Y8{VRVCvc#J1qB@LCq=FNx7D65Tc0YzL!F8ViA-n`VP_+=(H}ypkmQ-j2RcfV zEKBM8t+49q?NGX^xdKt{9?vHPaj#VOl4cZxt5O)$bc{)h7#vTy$%#g0c4t-8MEAPd z<>j*zT)#@ZpNUMR5Kk7d4a$&#nnYCo*YK!ik_Y0Lzr>YyUAOVjmQ#ibNn*lsJZMaA z$5|t857g3$iL1KDS3kXaj}&5gAmlkRW+A({iHelFay#X#O{ij>#1}kL&|m_r znO*40f?<|wRii+_$#aHCIV1mhVLBAL5Q1Iy}RN_>OJrMqi0Qp>G-)tdvaYju0 zrKvQlJ)(}26$R*8B1uew!xk(Kt)mGA`NJQ#jCo@>PZ@bQ#W_f-CwJu?_=qMej$Z4a zp^C<>XvRx4)$iNJH)0o$j+VP3)T0JH2%QtL)=`gcx-|5IQI&$tm!(Mz*LsPF#(5mO@-ftbdtNhr-bh%dyN$|n7y7v=k_ z5?~T+{{H|G5gS&ZrsTNNhc9)jli$<8S7z1eH%h9gsJm)aHrMg_ z1Zr^;oRjW1FL>=Ak1~1d_sp&r;kHcZg{Nrrr#3BCk*dURyg`T&sH{0rzm*W;UtlXK zFH7y(xh%Vy6D7G+7@e0Vmg9|(@-M4%)yLICpn(t^OBOu^G5?2*LCE_I=B=Y$&PumtXDtSazxh|5T8PuM#D92VJAS+GQ#$69OGV&?bIGb`UO4^PaCJ-`9 zT7lllcDW%#}Q=RU`75H`Q*PNvuLES*co69XZR|cO(Z9 zCM8PM-Rm2o?4sz3D-ao#S=i`pT%X&6ty_7OAC;G_v4+`Qyo&**Ym*naxs>tB?Ic8- zKqx0J_UvUcrj?FIidGz~Z%R#~BvhT&R?&$604Vb_t{Xc?x2sgR?Bs7`yM82EM6=3K zcUO@USM6X0yvbtU8P__Tagzvn0>W}($0)nM1(&-#JI)Rl@n|c*;3LGszDUTMuSn{a3; z3fzcFq{=+Dk+TX!?{%$G<&K<3My&3PKufzrrTLxO$lkD9PDlLIy-l?=i&As;qBUC6Ko~Yz)>ln<^u$ za!#RFiZYVr{kO^-FD))6K4a36J_|bD3mSgkW+0eFH>V+ERs=^4ScPKDz^F;k@9;CM z-grv3`xIuAJIAF5r{FqwGs3o)~^icLzMd{0iv z%4pP1vP2z=aECGX5lQdAl64WV+oi)HHxI2w7U?CM^1L55rBFsG~(V6x&&8y%3z&+QuaXnIT(mTbo6Mt`H=QsJ+@h zC&WNI^wY$xj$d{2Soq{B`SF=td(1>jnd}NebYi-IT2D(QR+oOHqv###jIh<>p@Gl9 z2eT{m*`IJHH0A0poJWj7<;o-|g{ePPymCcrJJB_dR_4|*l&Hfb_Ki_7@;i&zDXiD` z8f%ZKK1~%W_Fjf!Xt{Gh)Z{f!86Gj0m9;D43kOr&`SJP;?<93F!6f(cPjyNydE2~s z(spqgr?eK_7-IlEXzr}3t7U+B%PhSo;i7j zm~oFIus%m1or4_Wlf0TSesfxy!*(Ur5ynvvS2?F8Ls%*|8ApB^32g6xn`u4UoJ@A8)DyMsIZP`sI$>5Wf-6Ra7_uV# zj>F)9utWJ5J}!zJ=OIa@88ap2=akGSeY>6tDU=vYpTwmNCSp^VsIK~vHrjGV=3pq1gEcb3a{{Zd)cH38-rp&+Z`1T*hLn{GL)aE5;$EZWK zq9F!(LZgKu(=_EeR-2?MAmbSmiJX}wPdFm(I&oQAho&7!F>2{PH`hn7Z6 zggG_?fPR;FZC9OYjEMlo0IRnJhQ81-Ih~?VW+;8!_Or%*(PEJw=aESSRJgt<1!@j#DqUXSEzu)mK`T@*R{V zpJM%qPuv;JNZxv*785Gqq~bD8Oxe|1TCxJgj||f$I9JgQHfAHnINBPwWL#EMDTc8% zq^<-zW15_M@SUmQ-*8TleUgaOBIIdGn3<42kpjI>o2O(M*g}Zet0-XX35FLg&8gID zp=CwRTvOqZs%>C4JgF+oXEBBQqNag6*D+w|OlDj>q%uAz{S#3^BAv!}2 z#kXqZTjZx|u~@9tW>cCV^rcBu7E!L9`C4R?IR|~gd{>Qyo8uyev5%jRpAt2pXh%_4 zglWe5pIO~eB}&dk|_^El4Rl~mD8y)GcsZEde98T zrRrzO2<_v2t3g96&oeJqjO-eSgmQ+`qAN+5J9jfJ0UDM5uE;~P66uqQ@sA?MzC3$; zAYwKnREs0I;I5BlF1B-#3DU(NzqU%qT!!r&WWsqj1cc<)k5!PW9B!ei!O$=P8z`hZ?C3L-3}iUt zrKd}L)UE1!&jUil_Os%*E^B_@BrJG(Dl#J{`xyXv>$Z<6Jfuwa?>D6SY?6{25DF}K z{C7zyx+IvELbhZj6Mw3~m3tc~?W+Ff4R|qWn9{P1reP>cJnW;8BgZewo60DZP1%q&wo3MV9at+W;a!<> zti_SxCke+B^jqNJ?vl4|iFOJlbD?uH@fYNKMPCk!$F%CoTa8lCPk60WYGa(I5##7t zmF9vwSwoc}8txabC0JYRQ_9lwYaQl@f^tTDio@Q-TWXPO+=a zKvYLlE5c_P$A&UZ3Gs&t67N_$^88d%&UrT$xlbk|169coDX#K4FT-E+X!n6M>B^m> zkrW`rl`M}rv64`wYA7TElv%g~kv2G<2#dG$Ro!TJ*hrtq@kKJluHsaxNnT5mTyH6l zOZOVbDjqpfrczTnjpGNOHO6=#d&cHY{wAVGO=k1EveNT(AygHm+SRokPs3GEj26%q z@mGD1)Awv@h8A8ZL^UX(qpV&kU6RSUJy=oR2#u@7juSJ8la2BUYT`I5z=&IbCS!9E z6RHW-tpePvuUd);v&2tTT#hPEP-*qGSfagua7ip^%BZcn{H}7c<2uQ#XC>x%%*$_W z8!SR4!hy%PG{LNn)^JIAvSu)zJ9#Y|dD_Q%2Uxhkf^Bu)-i@7SLe%V+bEpA3Tu7xl z`fkoZ>ZPSXG6>eez7%8VvJB2lshB{;n6pa}Cfy^-nPWw?BV$^tovA2;&^YneBw;ZT zx>XHqqy4ly+TKS-T9dkD!ZmQA86We08pYs^IM`T1zm)aXdJe>6{>p_$^+K1of$jRZ z%}%+)+;Z(51 zbQKjmgv!&8N-tV9C6#9)O3X4?vv@Ils}5YavC>P5cLK97Z^mn5H7d@uq(RhA8HMSw zBjuxnWSdMCJW$_pa-xX3_9KMuYaQUK&Wlh#Gu%YH+IJpe#Z1{!NYtB5Q9|{d7Jon6 zC3qqtOvWXXE<4Io9jiEm&-9j43ryW#ywoG9Rjd&|Mj7c+UZ(c>JM5F0u~c%Bj?(+Y z8Pep@N?L-jUOUD2M}tqYiU^u4$qaVtY_1tkLD@!Cl%3rDI~<21t1}C6p*&~d&wneO z#T2>I%LkhqbM5ftEh8sN8Z<7uDMI_sk>1}b@ApAW*_H*2rzOR8Pz1wr4BZ$0a__et zbi=L{Ng%EeQ)kO+q{BizPlcMn|U+?c#ES)Kec7-bGRK2eqam zRB4CLkCa$LSF5uqHdxYWs?rZe6H%9~HlEXEAzS*Y?^I8ZtJLF#?aC|ptHWCn&ZfkX z1|2J+0JP;u6o5&J$AcWGl{n7e&z{9RWysZtN4I=i-A&!SJB}$Je>9i}4E=UMFq+1f zk)w?2p|;&AzPM&})xM@_Hp4N7I@!BHBGUqNqsmphp_;BG)PPF$X&A+hF=L%b-sr2# zkuXV}UVjjyr@M6RGZRv3jofl1-};YagHn$#*5Wg|&5b)jlU90GA-+$$Z(;GUi3c6uYuVprIuK629Zq}K^K5Yk3; z1&B1O>*1xDOqC|bIS9*5m5S97k(UyTOCDu7*~zO|Y8KdF0fn{H1h(|Me|y}*)# zaNCbs@_R4n-=+I+dxZP^_?d*%h?vquqeSOW?GkOl6Eog-!kka){{X3F_Srv^_S4go z9Mx)!%~cNOd~|I;zqqN0Ub=66&-=CaNAG95{X^g1cmCn~&+d1zzerzpe_UUG{hRg!?FXm!de^x5AH4SuxcN}EeGAxr z&EZ>)zTo$-xalG~y)%g(M8Zrm*O|-Zakw08@_A81o8Ei8KFAk)QAd_kZ*;_FX>weZTv;Kh(#uy}Qcf&+fdvC+&YL?f(F; z{krs)Fi%hSV)|sg^X?JuKWVMokv~)AEx45Qv(a~j_z%hEaQFV5`r!AuRvg~n`Amvt zmSx_*KVzrAw=eNq;~0NX{Rcm>#n%db#v?qH2}J(@c_qDnm8)IHI8UsLuEsdI-b-JfQ?=s8}s_S@Xd)9Jr0eJwds zx3+zKrPq&B^qcVcjNCXUCHZ`5e%SH;{{ZyAY5IvVoYy3LGsQ&z0Q6)16^#DhTfLX+ z9_QG(Sd=5a;hRMN0NM6mR4-A-Cd1@%%(}?J}X(3ZsY9#08zpEiLNYz27I1)+#XZ@8xAM5&Z_|ljo0eAT)6CF z#HRdTl?_*Qc@S=-l<5pZyyUu&iMQ;-c4AMj1sif@CGIB=;tf{d_`X?m>}-Z@t4_E~ z4RXlA3z62(9as12b#w5RozjkUlcy8v={br!Qp;=NMTl_(Tsqb)4VY@Vh%QPfD`nUE z1q*OeBifmWKbEXeh0{|g_|&9ChqKXLkyiAhIXZES1&%T<+l{3qYq&e#&i?9#s%34+ zn@kY43s>l}5}{1%BGpNjB2IYpdabYk!0W(3&8o5rna7UjBxI2P00fGu(z5Bph~)(> z87yBfB$-Ajh)3I`Pf)t@*;U;p3e$-?mwoO(wN95dwb@GXc_y88D{yWVXdAH7aUhyW z*fvST&-4Lvs2rZ#j&7D|0#?yPBDdj#Tay*A)v!w9%VT7 z#xx=()80&LhPiXL;TecrfJJ)CJ4=jJnsH^2r8cDc+RBIGVq!$WneCi2Fjc3|m?X0> zc`)f@N=c;mgQ=a{6}+Of{09?TXjU;&S+Yd*W=!OP`A7!zmC3y-quQKeCyZruvYC4$l(@H$w*LBSzygo6-L`C_cHDZvn}f)%Zwf4{$i8LGBUWuVqo_VQndI? zn7sbqM2wuJbtVFdtpdDr3goSPyUXKi^3@c?THA9ut9P^8v^sA)U1p-IE~Qy4s3ODU zVgCRhQPqUq^J0#kBAiJkY>ci)z56G0@mOBbwyI)Ga-`_QWOApa6W&b4sJ4FdIVxkc zJ3_oW>J5w{+QC>nC<<;-8Ss&h^pskXBsCSb`4m=H@%b^Q)rGBu#*9}Wqp&rp zv%;0FQBM3ml&7>^5vX0AV;6Y3W*~8qOUWH|9JxiN98IRPSysmqHxbcO8l4J3Ew@i& zJwSDrTrz^L3r%9eL0K69%S{+iho|+^bq>R?aDthWjoC8H9+9-G&jWMEkxLd>Jd$j) z({Yx*l&Yt_elyBdmd_zREX*lAuiGE2sN#Ldq_B2QI<+O%?p=X2%*tX#l4tRSEV|`N zp>@3J#U~~vq{@oGhE$6XZqJk=cB71bHFB<4=curzV5~7`Honh)zD`E28IsB{{v>Z+ zr5=HLI@*LV*@{`WbTX>PNshPOMD`2ui5C2f!JM*1aul+3@s&HHtP+h~Rbf*7q?;=^ zxKDJL@s6)h>@<<*gMjY(6g8ri8y7@Xch zb=z#Hh~W|xTHd}w{ce7_e$0Q5@3j8d`@{F!jqg|Xm$|+D=|64#HXgY19o(QskhmN1qzeo3fs%Q5e+uI|PI>UE& zK&BY-a3#3sTAm%Mr_T62=jtBY^w^yFk6YUulr*%N;bWyTxJNG;-0C~kN_<#9{Y&5D zpJglPKdBSf@elpxp448Ti8PZHKXO=51x6)ustq6E`&h4FAHjdE#Ntzv2UE4$Z6zq1 z@*qs|*YrlIdO0)b&`d_-uhaNm{9!%DR zq?kR_{?-1zQe^8wi|5xjH8s1&!DM6wg`&k9wt}SXk!!d`M!LsKfirzkXtztrDXhl+^bfr_IRiz!wka~)gvXut|O3B1+bZikL*CJ#EZ zh?+jm(HiHYeMoUrFAwfmna0EKzi{!xh)5U;)Fduz%cxIwD|f~mVx^pT^UfTQpvF(i z-RPGC6AE`W)nuqx%-Gjo-8CF za@Hp@+3G6EmO1iJV`59y<{q)3UQ3yQG#OF(6p1uuAU+itXcMllB?=mZi)Qh`RsbcC zkiG@W+>AIv$BQJDnq0~@gy$$AEM0idxeiKOvfCVmT_48 z*g2G=>aD_x@U4gq)g5cg?fto!=`46!g(^2yzSoZOQkG9w;x=QEP-D}VkrYvu4Mtc zamF|N_Rb6Q!Be#WEZTGG)4{Ne(*7yHFzoq?Nle%28%2y6>{5 z(qjPVO$gLS ztCXimX=a0?vvH**xWQK;xWY6_mRze$vx4l&V#+=Ndz4ehIIzw)(Yuscy>wjn9~@)_ zV1!CNz|vc9sgtK@v1_P)p$%RhqP9!DRmqO5R>l^jp2mCg9XS!79yml6)sRhUy?lVU z%Q2A%Ky0(JWH!wu&JzVgF`5#!Vm5keB8y#t8YNql%?D+!QfCn9;*|+2Zl+Pb%nHnF zw0QDbM6`kASk`2YG)$@6O*7VFCWwcTR!EM(!4y$^rpg=segrF`7HP(u=B#^ob6GhI zOezz*@tgku)w^(!+lEwIk)m^C!jkBd<*qA6w&T*j#HgzA09Iz@RI`;j$-^xyDv$vY z5FL!$j=>A5V?CCNb~LC8s^Qv%W1N_J)k`4=^4zJ(Dya8e>oEdBj__DjGcep`Vq$Y? zI>g(Q5^WLNByPx(sTlV#AQdvP)lsb!E@G|$Nh7Z-vTXNQpsOl532wtS2y;za#aIS^ ztr)P&?jm-D%xLWVr5JhrGDL_jy_I^Dx!69Ng-2kLNknRzSKUUT?N4dr&LwP`!6jMF za;vH1nx_=bmD0@Oj|7^nUJ9h-P~#~~1Ms5+4skgjN>a^?xvJkPn6#rFT-`C5tJdNV z3tgG=XX~k~dxkAKh*1%vHyxhL*+m@0nNvmfFBuq^0+Yf!l-1jdXn<%uq{;HH06{=k z0k`qog=SQ88BbY)gBEagGczf7PtQ~KBto#FF$A{^%##SlNU``GSY@bF`;Y$sHH~uB zNsb44ma8#f|qkk0*JT7pY2d|o(cBV+16R5V)VdQoH z04tfA7Z17!s#4PG^NQca|uB4>%EVr$fc!I^{9tY@bdD5%>j_b?*!%0Ly3-F6gvo-)!z;lFZu3|h?^ zJo@U*baTt9g)pSkQ87wceiWQ=-7}VAGvdpVMo7oJIrB+Z+5{4K^@M(1KK0s|Qu0sR zcaB{;5NLI7srrA4-L#iI)hDeBN68)Y$W@VNT9>J$833D;Tq9Vh0IJ0dOmpSdsH)S~ zDvhr!;l-aMlNoV{*4h;}jH;(bW04cS6SoeBIj3mY|d>+9Qy1$G69i`FNAo9xw?PkBZY= z4PIKz*(hsNb|k6F$6e5f@il5(pr%VzNb_?f3sZIaik_|~6;PopikTp^P)!?*)NC4% z9L_>0xg=qWw!!ub$;4#DvFSNdge;5S=rE0x6Ot#V{{TnpN6*0nZd`Eg{$ufI23mzHExMJ1E<9xW zi7_DkBxfIKt;~qxU?}gg8artyl%gRu+4zL8g{2;~n6;;CfJ8X$RpeL3Y1*shqXXE( z3PH^XYc5H{kz>T7LeTlCb^@l(_t}Q**DHLec05KL$}Hr|GOsm^eV1VXsqTc&l7vx( zJz1{BIQ2zWW4g=%G<9}SAP9@)B&;m7yv7Jp&1Yv{{KBOL6ki3=D3n3`uc4Rvb>e0w zp#9a2W=!WMevVCJhIh}udDc0D7ygxpE2yxVPmcXLGFqU?k&!D@M#vm822o)obyyy- znNo7X+Z~Qj)M1Q~1C|q#5wvel#;3WCrCYt=4txq$xlCi8-CW>CO;Y-5er%Tbq9g69 z1Pb75yq4U#nBt<2_VFB=UcG~<8I%yL&ATf_g%FjI`TRWx!AJQrv@>C_!9-YK&*axGr?^8i zoicc{#;znv(R_J9)LU&~MHj~m&gq6Wwev|TeA8+sYqbf?pDHMHN^&d6I6R0t0z(F> zm1-STL4{katocbY%)Mh(PXn?7aTsvnl&z6Usr<-}db5RSlNCgd>J-(go3oBa13*+` z8 zBL-ZQp6L`?B(MRJ{^A*v{^2l250&>_U4uE>CN7Jd&~uqh#`e7XH9r-DDHec|LWzqxo6AO=Jz1t$emPSF-cxi&8}OM+l)+NaX;H8+Qv%bR}D9}7TuHMvZYno z_A;nCves(Dk3h-`D`<6O(Xh-LmL_E23IPzNDHh#>gdrI=Qn?cj=XJegNF1t-W5VE0 z`we`Txz@Z&Rh@5ZbmuuD1R~a8)R$!uYKrtn7vtGj*^scz+-e@RExkZmGA`R8&Z0ua z?b5|^Z>6bfZw7v3{L(H+gwZ6UQ)2OgZ;ymXv!~Rw7>##PD;wpxo-mUqwM}bkN~x}8 z$$PGkuvxf6OEppoibWWu>AGrEu~v8fs4?Z7GO`aC$HF3I8qxtRq9I1=K!UFEIvst2 zxu+r{se~Dd(J6uWLVL2`4cW-73iD%n-a25uH&tX&oU2m-0T z=-Q0%V+IbhOwYG4VxRr^@zJro3kDmml#l&?JBd$L+@Cc zOy`khJcEX{jmj2!hPf+!rEs)TutWP)44M*$kFyd!6uM*SAE%6wI#F)c?c;sIh?I&B zd(8|)!;~|Sla4Z@&=TM6HPMtORkAafE(n*Yi03t8D^1c_-h0ijDLIWJBVJaDwv}5| zRc_2NV7q^L=(^I@EKeMsnK0_ml^+qbycLO^N zqQ)msrK2wM6RKHg$B)HTYt8HdDaRKXVzHf>uo*lfvnYQ8-ySIKZZC~2mB$FyRAfYd z%}D!Clv}4*zb;6Lk8zjdB%OB1AnbfQO_a?=HU@^8DVnRvn1WTE zO1y#FSuLUb%nNk!X5}2F(s;opd{f(_?NYH!GdyL>3^Z}X^4d8P5#{q(ADk$uPlm9ve0UuF>CW6xO_`slZICOLD+Vprbs|h5$yC* zotkzmm3-9>do^e4-9D$e&NBZ1RI6s=6F2fgBae;Z=vS+-IZt=fMUm2S_L3B{imh2d zCN75m0DLQCh5LCX3e-xu8K0e!UlTC6L^9eYXEYq9as>~UDa2-Yno_e7QDSB+CA#D=RG@jSJ96*H zD8&^gb(V_k@ructv6a#8R0r1pzm7>Rn1ZvNnD+6U8MH-0?Fq_W5oNuoPie$`%zBve z=ZT2P5sw5+q^m@$9y1Y`2^nb$8B|oO<=|DIQRcwmThEPCbrgzV3^GqC>j^iX_HYdepCKI$| z?!Oaj^N~`_#z|g>CP|FBBd1xyd+I(K6+q(sxTVY?{=HnBsgnm&>mGIBxm^Nq2R4q)LM zO{aX39lLIX{w8l5tr0OBTXH6^#uSv$&^dCDXtJ6iXK27g(uH!QmdR<&U4!n0Gv&pT zANR6%ad(dq`|V41Ez41AO>7TsL5`eeKOqs8riLvdGc5h*1Wn0k0 z$<&96h*nQ!8%L6HODEF%2*~E5Hf%Z`R7ISX0wI404CX>*KcNa4KVw$!ksQBZXk_v00*2<>iW@Y+La|3r|_8zZ*|BF(>AZ+<6oYo z!P6v(J}`wZ@I6ejXQ;Z6nUKlhv0P>R@v)e(niuf|*HLW3zqa0U6*gN1G)64-Sf z`LgM$f~E|*n~5-KKbenDc$6|CD^YZInE;RIyJ<&iFt?XrV8v4@vv`EoVvta6ok1ml%O7KB`RDT(D`=G~#JrB-)jqQqn~ zlDob*q*-tiTJB#{u2?(iqdSPxMY)Pden-C+zk8l^Duzjh zq*HF$i!~zYSdmqlQfv}^`L+c#)RhNq{8^liJ0r(q>q z;6)36%LoHA$YRJZZa>YHD97D;Rv$1x68)Ta@7x%obF2788db786s9_Q;wS{7pi|! z{PYT8;`pUzdzrOBm@7nfTO|xiZr#bp!C>yo)QEvd$Wj$Y%Y3Lp%(7;Tp>*R>WG{6k zxKOLJW%)F8lH5^RQu1|jPHJOVa{PQ-g{1sLkt$5W<85!J5bYORw7U5$a_ExDs7lk2 z(wLA+wTzcm?QR=# zINE<5t?5=Qb8?4iS#83CS1T>TN!;}`nWH5~Aqr7V2%1l2f89-7MbxB&em|3<1XM(1 z%ap{TVnv@DUNIdWAJ0_m&VR_gcbi z&!uNkRgLABkxb3vxBii|O0J8vNzj-}dX-?1pZ5Au9#ctD%#q{Kt;c@T$EjHdS1Odv zrJSHE-JItD z7LKhg5`KyYm?M*+rS5URb)h+;i`qeGS)c>iG7(Yo}l1oK*ub)w=eG zQEO5}At+~00>Jd!;fIJWj9CeDgwv^9%gjRVzUT2fx285q}7Ru6_ssbRc+=XA90NUrt30ul$O{0|N zgnW*hjD;gORzouzPj}Y$9O-e>wDD4?Keuw^7K;iJqV#Ds z8fUg_mf2cSqJgc9h^7_7^|z0Lp<4qM1vo{yUqzTIXP&Z>D-mr_snLz{tGP!VY?#bR z$BPtL*ZNGvcbT7?SNw_vM9xcFX?5ga$ns;!ML!iu&+)lgw7C!chIP?gY_E^og?Sum ziHM};Sh7X2(t+iYp%n_l`zkJ4ph|khqpq%Vg+?rslOejvD5BcQ0m6}MA*o!kMdB0i zG2;z(yyUgyFCG_Lg%8@plVhpxDa4SYw`!S|3R9K%26jfSF5}={QkiC! z-g8&$ZcB8K?L=W1+%m?bk>_I0>N|CsV+s@{6#6S3UvUl;9kb40qREp`e;(l;9z~~Z z!QjW2c`*cV#!7dI?uGR>dymO>Q!(4QjiYqD)|X0cq=GU%=`efXSrnMeqk%P&9rhz7 z+&6~GjfjNQ<;My*tps3(B)Qz9ipFL|K1@P{9M_Q|46kVA^>R;muaiZG>6y=f~~-A$xkmb6+K4P-INo z!Et>qD!o=_!JlyzsVg*e;=fE-Tx?7a6J(T~_KHGKG>YkENgan)uO$!u#P<YW_FZl*ZQN1Lf=u8 z1TABU?1padAit#;tdD7;~(ztTby+d36^FEfRcj+Vid;Xlh#eSr{bD8b$VEy^~vF_j0H`>3jUaYtO z0M4uJSEu_=+TNGwJkMD7XKO3^3wx81_dEA+_T#HJq~F{8&Sxb!eyQsIp~fF$ds)Ym zHe7?}92mFmxBmcx*Ux_9GGoJxKZQ2e?f(GHy>YYkzt&Gv>Uy51)b%}n!`goT0QcXp zxr5X_i|=1{`!AlforgQxe&qF!ZSz*LWTdJ&K99|diY+BJR@)%VGXDTkh2_JCG*>ng zoX78jKm0zd1ayDtwvGlj=U>^*_42Pg3K1bK5X(OlNLyE0M;B8ErYGrsga4!+XEbKS0P+ z&-#8WaX*kfKmP!gu3ot>ZThF`#S@3!Jd^%h{{Z;Adgg=fFZn$G04v|1&L5-tpX+z@ zv-W%6KW};7w=Y5T?|*wc-F}_zZg;u8La^j>2NR9zzS{Q}t#No}>h`PF+Zc{SUvd|z|>9r>K04T7s* zo9R3V%+NEqn!Qu6`_JH?)&0Gfx9R)A{tB=E08`X|;cM3^?Z1hCPt91%Z*HgeM$i5u zzx|~8=1c8g-hZ@ze0{i{?)y3SFWBF6{j|nuPgwRZx&0&Dygn`Htiy-u59wUGpHAn1 zln=+`30`eB4xmcm0`|{{X{pDE|Oc>zu=%54bY$W;uuViG}Og ze_6dV*NpXFMf87NrB$r6?3)sH93MyY-Up>{mEMcQ^xhvIgT-1<<>)y4S)zwx zK29=m^-5Bmr7C~d)QE`f9i!I&0HO6gPpRs9;P3pe{{V&WwLh=lMD-84A8i-VzSaJ# zy(iPc`ggrL8;R|2x$xpD{@>f5bhx|@Grf5$1!-`-auR-j5m_V2PJaq8<8k{p=wHNH z{{Tuu25si{DtH06URsAE{9_{x>t#f^|_d*_l!gQ4X0Pt_Pe7*;% z`Y+!90BUijsV)~GES!WD>aVU^P>&KmiHG?w)Un67_CDaUzH9dX0GRxR{*(UztJhLb z-1{fd_gG}3{r>>^6Y(e4xxVp#)jRhS?vJI%?YHQY^mpzx`@!!8r=)v-)_v>hulE<# zJwMfXY_a1fwZ7H+8_y z$EuHZ@=Y+|m6Cow^Zx*bJ$~o>TmJx!{{VdblKZ3XAM%U-RX*Z+C$+xxdiO8Zy?@$1 zm-mm}UfcA`dso)I*}|-SOYTqHzg_iDP?x9rKf3<_@J>fFiRm7OlvMOCEuHNiCp(YC z;reGz{d@Jl{UCqzPw4p9xW>HDGdHi`AjOfll>Pqz9z3U?{V(+2i(@nmb9ln%Hh}0{kh459=}lc zn6u?SJurv=03QDU_?Z6ym)B9qV>IvC`&W-2`OGi=Fn{q&*T`RbKTMYWJNx&Y_Rrq@ zzhwHqy}b%*p6T@8X#2m5?|v_}{am#c-rMz`Tl80dOTVx2)l~AM!Q}H7iuC=zB8khD zxbxqNF=RIeYyQ5nr$oBW96rq=J)l_QHHC+|CwfAju6yr20A`$zgo`wRCdeWm?AebxJ`_uK9VwYit# z{ownZ?!R32t^I>PT;lPg!l$(TrRn~U?WN`VpR4hC(BvL}I-ak{;&F9PO_FkXG34?h ze%WMV&l$!((I5H$0ONfRxHyBTQ;~j3fAjwUv)3L!T74(f`ktq+=w<%^Pru$zSo^vD zH@%7Jp1tba{{UO{PqSYC0QcwG2Iu;pIsHSJzL)7-k4^Wdw!Jm4Ydfb8gYAEO`Y!{C zCe%ZR>AZN!1QJ>6zo_GZ?o`LdNQM6Z#{U5HJ#&7+%l77Hu_eJFa}asL3NX`lWB>#zR+5#Z|k4y;bsw^2X#E-U{4h9~~-u21xw zvya_V?V2_J0Bm=U{)~Uy>#cwNMPIH?H|f9hbFD4%1^(w{7@Ev}*zu_!7r{Ax1W=-n*tYH&`LT+`e9N%^1tH~!!B_2B;i#NX+P{{W@$(RbUAZ2Iq|-_yR} zeaIZ2yZ-=irG7hc`MfV&_n!-cKSlH&Os@3(zbn)IpT_q8027q0@h)ErlHw^=b+5`F z?N|7}7x7Q=uj-!f+k2st^xtL=v-Y%BhA=hMYDaqH$evLIt%=8YX0aba{{XY!(DMHP z5B~rS`#)puJ*=O9?h^MoqlrHd}drzlilOF6Cu$kiL4o>DDFm5 zg5a^_gr3W$imk7UZp@xv{Kjl#*x zPoD`>4s;D{kcp|Xotth9MLgQ?s*)b<#Wy$Z0<80!%ILchK-5ND5?rdQZG_aAc{Wx^ z>EcT_;fhq&5qu`@CNcP+pmUN;U%C!2V47dQ4b>sJY>CJwojCiTCYnA6y^kzI-R>dh5TN;im zIN~y5oO79PDswxZJszsLiQ5<6-xx~sy2;EV+uF<5$h*SKY{IpRPmbUT1ZGv9-DCya zh3QvKb_eKQV~ZEJ%yF}W4Yl69oZEai3!4E~5tX@%R1{8UHXNk;j5CcbsNCtN+N*}w z5yn=`P3tCC2Gw_Lap<#@_`#8$Xn&?{CsNskejc(N># zLCc7P-pQ_9Pb`Iw3$)b#0Ntb_Y#J73URjU}PCs7jQ`%C>rCl7EtJfr%gg__esM|80 zc!wmsai1B8)vp(%M-lz<3eJjC`01x}>V`pXvRx})Eo2JF zucttKg80h!4CWdTyIuAv0z%f06@w&_NyPKiC75iU5` z1jA6}TVkaB!^zO*Wo67ktV`i}e~j@h#jbTJnt0j_F=bFSj~v^kBpSlqDNF0}d<2^< zYZo%;CGmAm%F0y>LrPr9IOXLRjY83>)U6AaqcZ?VK){yRtFdLuVsV0T?}-IbcUpgr zN!igR6XMyvr&41y>M<{Gj7DtSN zT^M%KPSnGL@}Zms+jU>+fkP&HL{4np<66zB)7HZ5jZXyO2DzGX_8xW7iq_`t8p zE*DJ-Y9$P5u6voM#^p{>XJr&p573Py(YJL*Wc{k*+5?L-0L;L<>L^0)+1O{?mP#q9 z$!$~%b!A4jynzVTGI>pR)UKd2Gg&K>+iNClkB=B4C9_{X_U;UK`~hc|t2~&7Ba$m~ z)Pi-Ice4PtHDxg za~mvTCBv{%UvZFlPl@1!E?jI z!c6v*p~-ei@s_N;qM$pvmO(iz+SZ$|B;ulTnIwV_{)@R*P86w=+(F_&2zXPUD0T$T zc;OKyR7BLII;%ZBr@6_HoQWg3C?gqeeNc&ALGx_Rg~4B#q;6Pg$~8k~}K zs@)Y*qqQE6H#+HTw}X8k<-R^T$d*aiCdv|?D8)frl#RfS(;Op8#WB+9 zV!n%m{C)>*^1xSH1 zs^5J_{rAe$*@|t`7_drZJdMvLZH{@J!hR9e%2Ly&UHsIjudLCrD_Se3SN+fK7KMpe z(#)`4JcU6(%~m>+r4e@1YA9e+S==h*shnN{PDQG`uktz_$xqKkI;zc1c{m;&zca^i z;5J!BDc(_VYVK%a)ii%;F%Ld&vLUfGNJ;D+)%FB{=AZ=t{{UP~7!`kjoVg2e4K_9xOYjB;se~1|9g}t^Yo9YRNn$y& zK*VJ<&8wheG1fz5CNfSqDMZ4zd6qlRaw>~Wp2ne-#^}vr$oE89&aQOxdNQ*A00?ZZ z3N{g-h>Dy<tjU3j zchoScmuxX-%3{BD^~vSzANSUyi$zQ@5=M*h2KS!+I65aJ9Htc=|`l>vr+#72Mn=QW!`dk zS(K^_`4(R%bUsL*R#ckU~u*x+R&)Y|NreU$?Di$w2(8QQ$$FARo6|QCKpwKB~c&WDc_?ey0p9ATin% zqcW#dsg%sVd@9#J!TMX-FJ3Hb zN?;>TW-X;ASi~@Bvk5Xy*qNwSQ>?c{ZCF*^vIQ)d_H~fb$jOY9Jkv!??~2rA z9EBvY6D^?}Y4RW75fx)v*R_}zNH!)BLtCtJKbh)EF4%Q(^bnSrb+?Mm7RLI*Fn zmD4_c1(A4A4qUh;^vF?=nfW4T_atXY2=Ra&OleS30rj3dh8%jM(amt2kjP0!51M2( zf0kuscAADAZIi31WYL?*=StWLMtZBdgi)~(t3f;6P*xE$49O{v4zlE&c`@Vm{P`2v zPAY`uLMbTH)p z!H%CRaQkh4&yZuKe2KfQStcTL9N9X37y%v~yUB>(O4E(2M$pKeqT1~;EXZpKusmiF z)i~)Tjox+$tI$Zx5GiGFtc>C;nrs_OjK1PF^Pa^|)>$S+$=dkX5X@}(6K5MrKv#=d z6l6IUxPv#>fk<8rzBL*pO1j6|-NHAw4I~{Vj57OK5+u@UmX{!~C;r)WZaaLlE2}9d z;dNS^2sixbx~Y_-30;x5&lz=P(E~=)qPvP@V7U0$v#~~P(bl7~$g+0iZ<1hQK`6Xag=I>x zTucQ_Gq^coq8r%EqZzN?N~JDp1d~0ju@Q8_TX3nW(Se#N-{@L*Q08vkZ$PtC~W@YK1|rv zl#YR_o!7r>LnO)SUBs1MZ62FWAo_8c7_wrrAWTPdR`*KQXKqX@8Qw%H)9SYdy=7cm3syfcTfRWxF4GMSBV+90fcJ2kqsl;?Og zo{ZWzqECjt7S(91vQ>UuyoIL600cuwMav~NIf(CGz$MGBRr;eM+k{sDb>!-#1x{?f z)>cUOH^{Na$+4RgC6lxp%EeDQo0dT;vfGg2OD02;d3PQfJMo4DIgL!n+&L;aUa1hK z3-r)HQP$y7m8GRY(_n(EffD4}ugL3H)j-W3$fh*6-caeK2G!>^kp&CS6BHt3_f=)i zI*BlSGZq|^j~vIf;)E4c;>mCS0Gag7VBD3WDh}rJw^yl_uE8w52|KpX6%|B!E1@$l zJ?iC1)X;y#f8@<|)+i{Ej#Vk-w>;b0?t3+4=^ryYj!E{jlLlOP@I`WDCSdO*i5%;y zFO7X!h^d{{isgFGR8>WytYfe-S8FWhlZ(e$J7Jr1#tc&>OqzwYN0BS1uQ>-w&)j0c z62}#0b4mLPDbrM16>$Ymlth_%vCcKrM-F3)mwT&qJ~yIDwUm~35ytJJ$E1zC)>67E zx|MYUj?2VldBv%oOp4L63c7|@XRG-))Q@wE6Ue%KIW7`4E~FRoyrwT4nUJ#apmD^m z*o`rJnVeYh(wvWWW@c@cwS-%f*v+D6U`Z+&%GyC5r8adkZps#ZJQXWSi58L96=?i3 z7DAvcHgb&flg(jd;x$u-C7!}@((d=guY&0#d(!3#oo z>KezW^3JP_5Z zGd4Dd-)Q~U6_p-&PUm{0)MTdmc+Vfl=D{#;5*$=XkN0cD1mudKvPmBNh?Rogju{Xj zHMcOMQ5~pkG*$;t(q)h=oH`E~!?d(< za=E<+Xq`ktB*fRHXqblQjY3r()dkhzWp|_znBlHm$SIFw5{T_3Rw@`~U}}IfmZ9ys zirDOoZoHiczWiC*8?9F&Z7brjd0v-K$1X6MS}_>rt&Y%Cxsp@<(m~R9q{>m{8XiA_ zm5_QQNmhDhdrhpF3V|{TmM5}qu0f-_4q#s81&SuKbNCXd#B$#CGxrg!md6wluON(` z>)Xy_-waV%_xN(O`)<&ZcIofEk04?0#pfd2h~1{@wk<(bKK zMPTa3?l6le`X-Y!gx%~9FSk1(7gW7Sf_UgKl{xUp_Opy;D1um89XT7`*V+Ury;`Jq zQ4u*07X0O%NA1*3NfpGJYG*O{iUsT#>SPBEfXd7BjhRn2EDCVt_P*oXEOMh}B`Awe z9Fkl$g*|c})7>06Ym)-WDJkDl7Q5tKr1dQB6HN*0+}A}$P_B9$UF!vGRl#;U;U^M}gl z$c8mjon&C-4nHJ`T!fP`Gs+3ZRVY!KI8d38>ln)!a?wVTmbuB@bjMR#mZDat@4X=k zUSdgZ+F;N!*o8}Wb-8AgmB47L?9SN^6p#j_V2nHA%Zmx=j#COaMX60Xv?jIT-)%Md zS^8ES)0XlIXBYfNlHR^%V}w98v9xOEzC-B5gU4zbs)c2$j4-3_`l86b$wmnpkDG5?Nru1JY|bgU2s5#)7yi<9a%<8s$#Xx8ocmf3}i^loQAfQ4^_)DM_vu&p`@Phq}RQ-PVr@yY!N|Og`l?@Zp^BJl}+}f zol!OMEf-B4hsIo!3&TVR})*44qE9OgccA`d2>> zmt(zIesTnQJwiG&Y*Nfd!%^63o4okB2ptLIwi&@!jEbBQRu{K}kRM zsZ`fK0mXtQpDth{C`TGlmn>=^newPb8R{zh+v->2luVR(-SOl@4OYTZdqY#f0Jkf$ zoxtx^kD8g9NCuSx;@C2PuI8&-#41maNqmA48&jxBk}0+hy0e~DiE6JlO`$5xiz%Bz zJP4IV<+_uY0kqkaaxlN~-1BuO3F)3Y$lFm` z!LnRAb`Gn|J8>smq_X9=Pawb3GNVxlQT~wV-+l#aC4=x5nNFXFj=+JIPgMg6iXil;l;B;#_Slz0$U%#J)shDie_%nQqf7 zoyPUyQK2ePg{akdT0{t<9a?lNkOSI^h19!f%~43vu}4y8s+SM7H?CQ=`dmaakess_ z%Ailq#<2&eYDCOx9Ezok`3QqPph;U8UGC!CV_oWI6hm6l{gtjd?7=2Sw)pA87c3H^ z@RzpdjBJiWD2>bV*yOV8Y%*GzZ=zA{R#TExna2`h`6`df7t~~E#VtHEX*Nz=62L@7 z?K3r(UPg;sjYJbNYJcv|=T+<&!iW*cu#xRVN1t&jiK~WbBLrikp*dp=#x;-Uluq{nTGc8(6inXMGhXguLD)yFu0wps z460spn&5d|SfyXADuIl;>64Z$p%}6}iy_TnjVi?7&te(0j?p;{p{>Qrofx!DVspa| zJZl;;TBJ&!wYBnNe4;l3>A|u!f-%WeYBeQdDlUQI=|x}aua?;xV*db5 zgfg6-;3&hF8bFAMen{#J>aZsr7n3$-GV#@i9OK0tFx4omo-r}rQQMf&fMIz|g)^y+ z)3{9N&7vwiK&Sd=jYrW^i`z7C-z>`cMfoZi;TOp!1cWeQz9Y*@Z(G|8h--n+*d@xX zHZJ9A%;m|?`JV1HR_--B_=gpa-dlb&^1i(jlBQrXWvfST4s|<%fm#_eqioBfwxh^5;#ODLew>Cf(UllhIM6zevQVH}vp0RF zmX{HK6${fc%wxw5{Tz7IM{nCQ;mv${SKj zx!08BDH3JKS3A2srm`~wSu!cK_7!y*2EMPt-i%z&k0aZqj4;c|?iMbxv75=PM6WHQ z8;-|hI}6)VV?dFphQk!&Jt(HrEGmvy7H~lC*wd2JoQVbBUcmZ)vn32$#@0#420F67 z05n0%zM!&-apgy-l-8oE)`*`v);Yv@ETF{A)Is@}fi{|Q;FNc$PhL?vf-{o#i9Z~c zlOl1$WU>tns$4q|m&Mu1w`empL^l|V zStOlu`x{2VkWE>LiT>vz1GKJEl=Px2vKF6TRZkyBQQFL}1dGr-Nm+d1%V9<0fpDrd*pC-qfjc4#Av+#G?q#x7kY5 z?h50YztObHemafl!c&h+yCn|O0=X_UhK1w~DQ4c{l1l=@%|(MT(C*eP^S2#!-AKE_32g&8d{br!x~DKn_Dv57iW?^?GpxRvC>^FfJN>%+<^F+U|3jw;uaJj~EQ zzVEfFQft?$I;Xn)okK z_MLY|Q@*A*DZDX{A(DsQ8sjC2TbjoeUmiz|W2-XSv)%@=b4FR47%15wrnXmTAUXK> z*WfDrxO8yHLT4MBL5}i9<@wIB>veu|j|$q_LcDzzGWQNz-WZ-1otbb)YeTS(hT&#&MpWOxEeqGeWfzqBySzhcPIa*)ckWvQ>Mc_Qk0X!5Q$621HPj zcV|XsqBb#=bvq+xQ|9TI87)(gsgur4l*z>m>L10$k~1|XQt&8e%O+EitSv%Qx#bQ! zIHkae;$j(NT($klmtB^cvLz|US+b)S570xxYo^Lmj#P$RHr#&zx`O%0o{vChIUYDx ztF@9-uGPvlM9lHZAo1g0R~Yv_lqW|EZ@o(8Iy#iE8lBs+mPm8vwuk70tjnHbkk_UL@h%qWTe=3+oZTByzi z5onQJ%xkojt^KEG>A8csMq$jCYJS~j9bm$8DxH*JsRFT3mZF9xNkg*v9~BMbhHPN0 zr8yo!y>8MY!l>HQH9M8Y6a&k+y{U-JnPp3>k0hw?QxYj;_0oEx_%Bg4X=)V2E2Yhg zESs*1#BLnC*?8#FWAFk~V4+rEIaS_hQ~PAeoW$c}F(Y&M@n@^>o=6R^7*=!n2h-zL z26{wgQj(o_ox`h*D#M}*45pnUd=&X8A^7fG%4=`~J8f>)KNB@5_GJCKS=bMk#b=2x z$aM_a20SEV$KQLx(S-*NmjjZgB52KPL1M2faivZI#nZ{|6l2wUP?(9dk;*O1&bhLF z-MI+^vZ!X+dJ!n$ZufdbES@Rj(nQ8A#~S%7F5kk`@#o0vyp`%np9)3NMcXXQm@)t{ zW#KS>16bKTH5m=;nolQn0UQBxP;x#OZ;=r*{{{TNWK=#p>-8e@adx=~ft6G}# z<2&G`)_uw3!*?=ir4XfPrgWM+ zEeu0(3h~x`VMDn#O+y{aW`ti=44oePp2iDVe;$4*IabSUYi8UDg%yk3-cGR#?q)wJ zEmZ8!J*edivX15=oLG4U#u~LlCn&TE6vc`&tft*+PX(^DMq)u>NiP2YMV}T)SKXRQ zPRb)(?e@w#m0f`^a#b%B^eoO3rCgBwhyD8PTZFwCaG6@JN8KKDYJVTe6bp6Chz-U< zJXyeco;yDu8S$fn$8SAL1PNmxI47nhdDb?JsiMX7qESO|S|G(DJ|cY_h~7(NSv*|F zASPiCR~stan+5QYY$0@$=xdw=ahDI?rlWIE0mOE(zK{3vbc9PX?bomiLA= z?;e=<&)@$5vVUzolj}bB_rKiVWqLoi{kQ2}r|W*BOZ&6Y0=&IH(Y-^}c@p8v{{R|B z=W^GD>72)x6&0;Qy2TgGzKrAdPdvynUeMzWYMx=)uI)+F7KNzhzRG!VV$FQ{0Ur?c z7S=gXqG~rTeiNOS--qIVl;`SG?|-;|Qh%rqykCC((rmu9_harKx;^b~C9U3lCxz=i zsmQ;h`UGe{o*$=rH~t^M!kaP%iL$poyUC2 zXZ~N>f9}U0Ue^5!eu;kUe!u?!P#%r?efvSp_UEH|-!IWUTiu@E_dmP29-+qe=d}Hy z=oR3{)xC4nPo;1P#*5Mj?`bRD`@DY30?&}jC+vQI``?cx z=>4y@$?o#>vFqV4yzxKd5i>vXlm7tK9_b&XZ~Ab*MPFd}{*~+h0Djxn{{U6LaD-)g zf9F>|Pqe<(dT%a@T9NeMw|2`xXB^e7vR(nUY{Ri92Gm|f0{{U2fZssR&jaJWfH9M@^x2b!Xt+ zn2(i;w1#AsY#{Dlg|e7?EQZ~E8ip5&_g zqm3WM@L!ki`SD&h@%6#`kJG*1x1Bt9Fb}5Nb{LX6`Vp<;& zp??TGrEUIawWMk}QrY5)?#@-7fW#ToW-J^j}N&7pHT* z7uEeg)BRu4eQ(pB(*0x8xqJ>kJJLNzh0a=WeMi&1ONGzuPInr-TRPrMrgjWdkh(cn zrIXxGty$okjlC++(E!222ZTb1gbw-0Y~z1i&_YM-(Gt^3Bms(W*t>K^d* z-V%C;u>?62x1wHD^nYUi0O@1<_wEnddWY&a z^&R%l?RN`aHz$$p{{XgrhwEQ-ea5k}mmEHKsQTqSx#`}O%xze9ajELlnDZ*b`i6hv zj8IJbZ));d-_+hO6;&sU-%3vstv3|4y#W0KG!;3s6Ekf=+O<66b>fX=_|GH9e{X93 zufOSeueZM4aXriWEBmwVFME6I+rEWqZ5OeG`Ge_SdQE z(~P}8)YiN@c$<3P8IhXF_J74iEPu~q>73ANgiJ(#mYTXV_^3a68>>Ol?th27V?M(o z&6;DFatiq082LNzsGeTBoqfvtVfPd4-@Scz?Z@43WqX(H=f6F-$~-?^_OH9WBbn&F zv+4EMX+3zH*&|ztX%(*@kv-wbjS}2?(k_mBUM$$X#xs`=NXd-kH{tVmcaJNF%;MUdepeTj$D-Sh$>VY&%Fw*(ipc6i zD=M;(!|3KW8lR6r6EQoE(;mO7ulV=(->?0~{R)1I{qgS3cdUBfxjn=BbNerEaC+ab z`p+}hJ!{MU*Qo}1XeTtXKoUxJPa>v!0DN)O7Kulkw>LjQVNw=r0dz&w!q~n>s?K)q- ze5>K-uw|=3aTAv^dMZANDWXoO{b=1#>H)c8U6^m0c3?nD0l4Qq}aV=K8+T2ey-0B0p5rp9VQ=y6%j`_O-VYizr5t7S?c}QeBkq`6#|5s}h`J z7{;R~w>~zgs8FT?T&qN?we=>uIMi++C1J*OliW{FCxR&s{FSV@#;wM`5kE-E^%zre zEl#GEVZC0EW`Rlh2C*nH1_R_eFCb_B46DeVG@z=?lc7V`<#MY8M&!*=A#-I(gJzA4 zc=6UznP|)r8#+7x02rkv95rY~Oi4fWbHwNQgZ+N}#Qy*tKc?Sd{pkB!?Ee5``}f>F zQ9` z>_4G?pO5uVb)Ob&nK7It#`qbwC5k+XcCmVy?Z=NPN}Olv{^LLCzQ?JKA}1zLC(Jrl zT~{?Lz=WmojYh3l9dN1t095De0=n(`dHppmhGZ0n-4$Z9DXO&x+#QI_)c*h=N$`#M>XRwuN_c;_eQ88S4<>gxmsgz-AHCjb= z-)djP-tAT%0g?pTce&j0+Dm)f*Oz}JoK@8tZ~CSG09Xxxe^0-r@Srg3Nd3>666}TD zwfFve3Z^m`02rNiJADcM3icy5PjMdaGRmU6T{Tk?$Bi|pT*V3_TJ~?l+S-f{{Xp6ey_tTFX`{}+N3ix;$L)B&>19^T#vW_kf9W> z$Rj%c09IvYui)=uq9T3PZGF5r_l4S~7nkGV-*vGF6n_-^*c6{{jY5rnR`OE5{CBzW zt$R~Z5fHZj0IFB@erMD_Prs(*o)vXSea@PcL$b-W_vj^d-2VVMC63MV+YEmN`v^t< z05zF2KPpfC-o(U5{nA(Z{$8yA02KSUlfP?;L7RS2-}0E!imCZk{>D6edL#b;RKM#0 zuqX8U`fXq+mQc|9o(j@o6p%%EN&er@m(s`ZKk32sKb*{&-OO(%9@OA*_-wx5_g^v%!K(V(z4d{se#rtd2B!Q+@elClwf!^n3Eu z02Jgi>~NEo$0m80tZ3-NJaq;wxtT=H_WSQqC!zlU*Q@&Ip1$h;02k+9@+bN%BRLEF~Z$#49h`X8<<_5FY7U)3gbqoI@J?D+mi@;|yiFI86stQaVo zwoBs(_c(?tYadZUJKSKtC9xk-Jbf4Kp&M6SL-bp>|T=5#L_?o+35_@TfNi)vUEY5dz>+O6P6?=Ts zI|pCHnxQVGl4gCvI>!CH%$XTX@!i&pDTXvOV$gU;BQRhNNI3n%K`e6?{Iy!hWoyEG zS88Ld$%2WbckDPL z1T9St{H$DEK!{jShRj1DODwaG++*hPlfF_2jcPPd?+dqyJVB7*RiQD3G;1n^NIU7q z`5;jmPmRU~U5?reE(S(67{%_Dc_rnEA>SThoiH3vQ< z(>m%;oQb@6>Svv%HZLh#O71Jjjkh(Pxf5yBiN}{)c6QETgEMTUfTpR2W54r>sr0fZ z?cmyT2r&7}DxY`2R_AOJ%`dZwuzei7WEnFN+y=GL@LBfHd zRAy6|@y{kyh@Cd%Dv-oAMJDgN5(}{YszGO*Xr{3lQ;tksV))S_IH3w*+N1!tU3B|C zoxl^5A#3U>)3p_?vQGCjF42w;XhIM$RH|om>$ZXhNX)foG!SLpk^E~P+&B|0WzMSq z0PAR}xIbC2hRTa59XcR4JnYmmdUJ00nTX+7JyeP2oITCGyl*sSf_inB89I{y*T%Cd zw&Oge3@OVHkE2i$0qA)b31xmY-nx0b+{G4UiKR^5K!)%NtNawysr9mC>EXv16i0*& z`0RB!Te796abk-=t@jldYZhW~@^ z=;FuL)sf_WCGfSz)p@D5#gjvnOD4p0L}I}?Co1yYmxR{3SfKM0Dv}^R>vx3c(HlmUYckzn4xA!9q%CuDDzD#EvV~!=q6=DyW-4c!t*CSlEOcqSU#iW#x__nRof{R^0@RdVK{^pTzL#~fNy>fDD<8{V223k2JGBsWH z*_D9hS>7{@qmgLv<$0oy@!#a>SvRde78Bq^zK2+xf;OZmvh9y`OKLS@fURCScJj5n z&m#W-GO#GKG=W6jEoO?XQGQkc8A*=LOAqv@*zqV{+ZG%vKbAtYl5VFK-cq4f<_y*h z06T7pGYJ^n&I6BKY)V$%^qdKqse3yq+BQ?3{o>kQqD{FTVhgOnmlaW=_`|iwye40zJ^=}tVktJ;$Zk&BgP^U`~b?i3_pZ*~GSW0^7EB0l2K+L196 zQ?N?m*z)98-B;tJky-22+HFM>ZJLZ{YLtpL8v$~kmK%=5&NA%H{Vq%_IdGC&!;n=7 zwKd(NvSWuh)P+ToPvT3gLb+5HU`FZ5Om0;#sn;SyWrEk{R79DOm}Z*h>d;fy8jYU} zifPtRBQAtKepY&noj>BVBN0`L~H7LhkJc$jR2Ce90spAsKhq}K@E zM6%`fcKSm1So(9QoS5L|wXCG!;r@JO9l({v5Kh7{{Y+SA6Xk~3h}43zD#2#3t-2YR zZE96wjSkYiW}~>L9w560l)@tK`*t)yt8pM%02@#tL8uOn6AE|NINv3WbwOA28$74! zg!Rc5OmcQy3L0C~BLNNKacpkV>Rg`hf<{7ujKn}%@?UwCi&E3AjuiYB*hHgU$ImN4 zy-Aq{uHV+6?AuW_@#D`-+a^qhc{wBSO!)0m0`EhJ_-|dYsb*SQ3v)9H!-{dUugYB3 zY$gO6icX~Tf~u04$o7EF*aU2AF<1xVkn`zfs@3C$pHSH3D+Tg$sarAZ+~YHkEKlJ* zCg)OZWwC3s3U;l+Pj8kssl!@G?$qmFYUu z9m?IhY{gZWt0J!6T5a4`^Tt($l@s4~pwX*FlGMW5EiVi9#WrVEOc=>*h^&*6ce+=+ z>G$vWM91Q-sFPqIl!(0QD6G;#oP%l@r$K`jHZ&OPQmlo8Q*O@>guw#pVB=uKqfx^r z$*Cuu)WMGhu1aO)gDxsGthndfoKO)bwPrphd+sGnnT1_&uosw%UvSIWR8#`fYO>O7 zq)UE9F`ARdlPVDOik(o^^9$^92VbBDvJxD)F2|!)U1+mLAAMAZ%nzB9rU3hPNwkA{wfR@ z5WVv1Gv!oJ!Qi$>nQ-MnN6+k`@-?^DKjfqF|9h7PgW&tfkyHWcK4-%m{EUX@inEZ3&(0{up z9ajaF{&NpEp(IeorgOe*)oTZ5Og$WbFjdOBk{to$F0Q%DKN7}p^}Hl}2cQMx576k&H$sQgyvBG5L7#T!tG{5xklhHPi3R4kctIWnq? zy#lQvS}eHkVyBCugdz=~?M%AO88OP2+`OEkkx#KJhCr{q#CL^0v$q`BxWECKYsg{^ z6f-UD6q`FMT8kx_U=PIn+xgS#!#Iy@#bm=S9~aMwEj<@qBUDg{PIh7|6AUlv9B@)d z)F{ZSNg$-R3Wcbk^D^g@agEFc%^3dRG~kh(44L!C4$xcGO1W?L*--gY;6KYc+&p&5 z3F)YEn=WDo{H~r%M%A5p8RG-aXjt*W_Zbvp7+SKO%9A&WH5WRK{5wtjVN~uAAcUng z3QYwiZ`+=}M{=y?RHCznHN>|kSC&y(6}9vFUP)PU8I9{L`6R+OusyyhWF4!Llcv#L zAYugOPbtNXCEj>zmvXu8(p_ciCnkEmf)d&+G+89Fyv)g6nOsre3r+=)ep;GsijW#+ zYG?zfWvPsDG+6qWaq_o{;!v?(Dg;(k7bP2*GEoY3eb!2>nKbaduE?4%?ey$P`9_iA z;LJjV)VEEpuR^Oy#5SWL3hg=w5rwr{Vbn4x^7CH;y>6M#BMy99%+&DPX1)?q54|Z@ z;XKhaZXnB~H2t{t(Ge$<*Ssj5I>$LcnPcZb2hVK_E3sLYjW!%_nms$_IXczS0@GE1 zZk$c30Vi~;w~%j?=bUrx?d@`Py~fc6K?9XzpNBo5WSv%I*w)Cirl6=QQIu!E%q13e zj(z440uY-igIQ+#;{pD@~ApM;a@PVT73}xd}$q(x($Gz+W3k>*r_{F`XTciq45T4=OsA zvQA1S7@bheqi^Jx{^+ZX4FncmoMg+Y=|yU^wsfp(V)jvRk#WK_Ws`wrgjngyj&clE zCJ$z{t5?O1xDF<7jhRhs2}wMa)sP_}N`|PLb*s$3AI#{!Zqva{#+o2SKmq{=8HbVy zvgE^=C8ZXXq}n>SkgBU?69ZC8#BzRd&JENa5V@_JNvuN}%MkuwJq%6WSy`sF52jud zP(v+uY1A01)##_N1^)oJ_?E{)teGTA##UUA4{do5jmdyHM}Q<9iYbP^PG=GP!B^|f zIsB=3>3&P;DVXhekn3VfN3xNrT20!N&N$FLOD-^DM0rz=T{csGOGmB5v}ICT1Nc)i z9JujU30A_=m1gX2hiawp;+KkcJF$>z(Tq&ivaqa+^0vmUKaT+Cx?XEuQOjr2mo`%c zK*$v>C@ks7nYJg4{{W>W3X0m7ESmY}2-gcgw~CFUdwyLwQwW{e-cp*(BtP|=h#gM| zEX)_Vbz{f~i6z__QCbMHpkC#Xt4d+m66xolDnZX9%(g!*9CMN5cyPu%yjof}Frp6? z1g(T2CES{q!0R9*jw2>Wlf2O_t(DU=*x1BgkCKEurP%}HQKrRON6AX2i$fU$BxTws zW>D4cMI+(1Zz83;ex@kFkKDq@sT$p)RQ>qGlH5$s=LROdthvps8OqGX6eDF=o>!f^ z+RlhfrnkG3yn-kScABXON?fc11$L^0D3hApqPqiyY$V7<>qb^j8L@l18`-59iMvtk zN8@Wv*1;sNG%YEClwr1y{k|Dx8O7JdiaMD$Iu;5qjCT*KBhIoRWRpXoSq5czFOHdW zVd=cnpl4>?tSV>bd#x$@ji*9jBk6LoH(620f5fz`gSDw*@pH$bb~wwCM)F3%eS#KJ zuAs8vnO9J4<6)0nJDXO1!~!V{{V)0IOqD9X89qL!uC1}WUi`G?Q;a}8&Aws zdUi~QsA#7QW^G!N%ntk9TZfdH+rB+c;x!Fa#3m++AK(-J0Bl_GaJ`0Xv|tC#xdf?S zhVVPGzta^jj|;X4$GKh1THiYHHH0IK0v;`R+}&}HB;@Km^#}g3m>P{~d8f%}TWwKA zdP0(fOfrs*W5<1i%~~;x4CS?gv)x{W9EKpGs)+d^pVKhfI+Kt`xc3`TSR9vdMNKLM zc8H%iOCbB(EsG!&gW(k`rNrNTLWNAcM zx{A9dxztP&&Nfk&`p=iBBTPc?NKJk;a~I1>(7%p5ixD+@^jPv4oN|PW8KiT07R(rM zW8z?HS+w{3u4l9rzvh~(dIVFksb@vx&Phd;rqpEupAu#HOrZl^HAiKO7D$NAlQJtc zqholoWcrA!P^dfXq8;W4O4Vc~V`m$@mT|2}J+&$!yH$T~b5yMkuWKcuSjKBNP zj2V&1C7zu0twkK7iu0usnwz|nEcb^Dr zoAN%(qaznmarNC!!ClkC8i(ryr6oR186|zr@>(Ux((s8i zN&u3J$QAi|PE?I(A7Ekj}$hM_Q4`Pa=*q&y{7%GX#t>q=&+J>Nt_U#G6!( zQxds7#idx_WsHj@Yfk+2^IenVt@tiblD`PaYtl7ZnvT&FOZJX&t@`UFDql>KBxZ{N zg$$N{0<%gw~5b*zmfHYdu(}Usf!;`V}Ou0 zLtS3m5~PSKEYX@*luYrTOoDO3$nn-qs{mX{?&jmROG0rxdod9c1uTTs6_P@WWkxK9 zY^hdQtlH>04hfq6eK%lJ`+N_TXXDQqGhz1F%;Y$iV!JR=5jWCiYgpVtD|5D_`k1lx zGAwf`V^ciTjTdc=MB8&{opaiWF>Apl?Z^%M2b7k_RAGuvuKZ?`*NAA&$_bn5z%?1a zqWGVzbM(xlfZA%SVLm3CqS^XJWr4^3ppnmR^pHI*Z_ zmXnxMHBm)FbK+MoOh`&NU`}GFnjNj+e3Fos_pEapoO{(iQ#mU`#?y}Ule_XY3u;z8 zfbXp&wq|F`I&v5w4y*jS1^NQUOtV z2>2;gbk#IspmclC91)nG_k97%>@{|)!mS(VyQ+7yE+Zkb^0H2 z@?*w}))>NBUmjKixR+B^_=r~4KLs4yi8;(NX3V|0$>UQI68!3laXf3@iXseyxtZW0 zm`kOo)Ndl`XDA z7^o(iukt=$BDkWtJE_$|Q8iNjWk#S&@CLC^yhI}a^7M{470vCzJ^Ncy_{WVU%y-4H z~3Sk%?&tD#^UXH?ZrsvL^ThujpszC%2?N}LeE zQhb@MjUF|vnOqtxPab_OgQ^_9PlGT(RhgxBE?KXF1$CkPLU7^sPyX2F=m3vjw{pd1SP3F*y=dfC>55bW2XSo zxluhG6t%FSoCH~N2OvfP3xWaE$}t5?Fg?s%Vnvl0;q2XMZ4qdMVre$fi7%7zF2}PSsUKDkp|2PjJGEw#R1!3s#o1lQQ&!B5b(1xz zTn0J@*&IKKB{j&SAxcwQtStp&S*owqsj0bBGVjbx&!V4jfH<*ZYa`2PD%1Y;B^YJ} zMUNb;D*7CCX|k+|-ER-MMSUoaGg)OLszk?1%~^$Y>!0q0 zD2ckP706&#v&Lq>6JP_g52}*X9OV?Wjl!9NVObc>{)DfDNVgs?h_at?II)SyZ&;x+ z(9xfnT(V6jUmmVaL~))Rx50@E7y&~31lRO!P zt5i@=bYP4WS#LYWlh%1;;_l!DR#j$gehFvNiyM`VL?|(}W=a*`$=_EhFOssaD2|h! zOo9$MX9>qwBaG@!lxkwk>dHY(%{b%72+c`Tk`0=Ui*0!ABOSuYO0$I%Y{KQLPMYZdAg1Jmzv@)p_h?L?Z{J zRRoJo#Uu|Wt+DZiVfjwli!(DBx_KogqlH$dX-|res=K$o_@}wWFB*ZS#6I43wTaYsW?iq4Hz={{TR4a|CNySX6zlv{4fDQSL7O>S4-*5hi5? z9CQ4@%00Z}l$x35#!ZCXxhGXBtX&yY5s!tILL?;Z?s1Z%0TWhMI`e)e z2n5@g8Mm45ltt+%&w+97bsWE@R9|bAQfPz9Qm9aEco8e?-?Ax6s!h@?%)1Al92YZo zU0b1^%I$-(2>$@_e*kkh;O5JWYho7B!t9KPZrR=Bk*5>9!(dhNE(_wsywX2>CD z-Y=NOyka(|mgAQ*J+&^rQ#pvSgcR85nyD9(Bv+BJ{6$07TS)5S))buw*Z%4HocMmX@2_3#_NUq(Wpb~1a=p{<4o4cv$8S-bb!dBU(%`&~ zcd6MK1tY_4@=<;Hr~-CpP6dW3|2_ImfGK9Y~N{X3DsUN5x0 zIi>q%{fhdOIPu`{y%wCmys=>Rf28{_Z~9N^m}A=ePiPUx{lDYs{@dET z^SI-W_kZ<1yGQ-Z`?dNP{{RQyU^xEB_7}GJ-lg{^+@6{_{)g;NCqIkK`?2?T)_9_v zDSN-}N4ifHo6&u+oY&=l-BvTzTfn^JfDU*kUhu0$z--q-|;`=ANl_PkFHTa zZ|rf@ohA7GQ~v-r{(t55)W`0(-XGQX`Aq#s`b>G8KU@2m_Y2&dy82h!{{Xh0(~I$a zqwH@(uOS`yev856@`oGSo`c7mPM)Lc{-JedSpM(TdF&H(<^aR(G0b?A^FNgT0O$L9 z;#K`t+N67oz(X!L8|>uUvGO zF@2Zzm(#gIcZVuf99H#TcJdN-;c=J#+I$ti?EOcNpK0Q6V?089S> zF}^*%4{M6~^_L?2fBK*PpZ-@^J{R5Zbo(>zcdPx)`z6lxzp$L^`sbE3aH|&65^L$%Rf8J~f#-{{X%7>zVJfYz!)7n3A2LfqSUL?KU?P;$L%vqziE1&4Tqsn0%y(;cmy0^Rj2>m17B=BT^r{rI= zn1%b0e5?L2$F6hR{{Rzyp@Ix$C*2eKxWD{S$NusQ*EpYPKhM|b>)x>7!|rdl{^$Lf z^=i#qpUCHW6utTFE-SDeJU?0XeW~hv`R%updK3vC?d7WV&w2jOKU%>ldrX+U@ce4J zZh!22BmV%>FI>;J{w@7CC1W|s?Nof&-2VXLj$iwpzU}sJ`78Ye`|eV&q5i7A&~R4B zu7loQ;~#D)15VTevHOD$OL#W0rd8EgGa@kFvsd6Ve++*R`+2MVBQpO0kad6lUHx*n zzgzt$FLU~aBmV$8r~d#KUtak1uhgIE*Vl9Z0D^t|`z`A_$u8gcSKU8jdXBKC`gT9G z^~c1UJ}vwo_WI-B>0ha1{-^2r@&5qLhavv}<9%vA_u0Sc`^-y!*@gcA(e<;R`hoim z>0EQm^*_D8XuUU#8D@Dr{{Xo@%;E9Fzw|?HN3AM`Bzz3y{R#a$^#?jW$1|Vw#Qy;L zpG3Xiv>n&F#$rEnkN%&pQu~|r@%kqAiqro98~s-Of?CC^J8?ZL?vJ#6$LbKIDJj-h z^xtm!r!MiRYcGpTF#*|Cf;%S6U&G(7=9uaJkM1@8SXhtzN(_3;{{U9~Gar|~!~Xzv zng0ObOV`7{dq2$o0QlbE`X?*VIi9KOpQtV)l%@T%?ccQ=-&6Fu@}%w2-2VV|IKGYP z+>ILz+bY~Br-sg~i_ZHm;BVqwh^fXuPyYZXmN)+Z@UK7oPp&iEe-nO;4y?V_zQiy7 zK8M!?AE=-6qxVCK{;Tcp(cjr`w4bFfPW5=d8=vlPLH4hF8ib3f2c3jU)+C4Pj~yT?vLNEx*p}{&(XcR z#PzRM_Yc1P&CccXsG=T??d}I6{EBhlSyWGp#6*nJAyR0Fy7pW?+uD09RQC8swZwm9 z5`X4e{{Z5bt`{zEaqhB8%aLZtzr4zy`#pB0{{S67;Un$$*k4HC{-QrzquSo#_s^nm zfAgu}eYEwTQ1t8luKQKR;c@=}@e2Ca+%Hn()Np;{{T2%qwkgX59#yt zS?gTCLH03DU!r^8@4o=w)BS(llLov#U!r}mASHdf?Q{X zudtI`nf#~3{{Zy=0Q#T&!oAnNm27w>hx`8k^uPMA{I89F#)tTX`B<7qsc4E^sl#)^$0xS>A97s$@Shut;LN$9~K<@rzLog_g?UO zegl?Y{nP&dH~#=j(tWqISyUwz#s2`sf6M;>)Ajb}`7HkcB>wD}+@{_eZ26Wku-rR~cVHRDH#NKDIA7>lqzk{$j@5itQCgwRiiu z$@*{V{>Sxvm`}d;_;TWXH{m@gGt!-YFH`>jy>0#b2mYEq{{VXr^&$3)**|u^+;iL1 zJ)!UKOK(v0$#ZMRr#I30{B@@r)xEpwd|9h>`lqJ)ym{PP@y~2lyjq!;R~CQIzkm8y zzy22fA^k)6YxS&ikM$4I+T?A_sf4nGc_`0?^|3w(!l|R|ez^9(se3Qize)B##2=>n zi8#H$P`h2vJ+P{ynC*?%;(LBEKE8=ns?`r0{?Jsii7P;aXd#&GJGs@J@KgE%-GThr zeI(`f(=(s?*-V2LhjM-vl@{|wC&!x=G7>@v8OZTL_X<>Ih@n0LANJZZVni4+B6&;> z+k@#ftim$FZ#hX4XOAQd)MH#Vk6~IBb$RXmu(n6RAUD)g?jdzEYaz`WyS1d&y6k`` zL8>!bbKEj72kp>sPHdK((nn|~YC9s8)rp_PsO;twSIY%}n=(dZ1J>zDtE`0=N}4P2 z-SE7DQ=|4e%LNLHX5+BxUy)#yEKw z^AK*+75Xi3n-l(CeNpqplraxN1ZmDWo~~~`B)TVjnuNJ^C&VpspR2HhfN&J?*_Lx_ zIgcTzomDfRERIiHry^AB-d{Rt{rq(ZNh%Q}vk+~xo*9y$n1f&y=Z4m_;+($R{{T&lirdNzClVIq&?$Pe6_s0dd831p ztfDar%+pF1*fX%m3aX4tj(Cseax9RBPN9l_Ye=)US?nj^aEB(zxp6CIZDPkxn%OgF z0uEDG8>Q4dai=mP;BvLD_Z0{Gg(%3?oyxjhCiA*Fv-OG;R2^(n#46#p1yM_@QIBR) zI=JN}_SK>)VO}z&%^woB3?%Cj>UA-XaB?#-9Xn&x%YkiNy>g@rM*c*b2}?yN#%!)T zIMee+kaI}25~Y&8*D3%;pmYP^Zki&M-ti10C$n;=8L!MemCS)pyx!3>xQ&`tU0iz4 zsP?myHa?#jsQOpDT7Ket68Fnu)6bl2kYglbU8Om!KvY>yoS{B8fzazGSUnn&4Y9{! z)GCKfn=V+)qYgQ?OxY%h+e1ll+!_2TX~pbS&bb(TLb#EZ#>B;C(7CNmDpbj*BVe8? zC0H{{c>tC`X}?Cyw53{9JZ>pUf~zLoB{0-kxi;iAS(t*%t2`HMWkKT4m)lE>c*ahS z)rmR|+F}wrAexj!OL>}%LuwR*3UTgoPx8uZs-3v)@YcFqV+os$+rk_fm^j^uYgGa& z(bW!2skDTwMV(yC?~RU)qawGHjE|zsv14o7BRx`0XJJfk4l5NR!2>3LD&;(8-H^@W z9uB4(oY18|+LBQc;UbSHoLX_lPym>XG%%8(l8!wKP3l;>IG^D~MNCWmh4b! z<){dlz+K1g22dRxt0y?>;pyg#m4SG^*b1S~9{t&g@q}o%in4R8aqaz)8HrF_4~eTL z87TG>+OTbXpMr0#vxiBpvJvB%dBu=X(uSjvV>CiT8%@~BhOEj8__8+3^sM9B^zuhp z;J%zB7a2ocYROC`9Vkm8zBY2|T^=&Dsvje>dnjRFPo9&F zs3#j!nA#)u%-?#`yjoN~9piSF6QYl$&5b!4;NqGsX7nbCT~>8_VNZLKnYh}`25a7UyBXw_d&3xytq-k=a%wpcK>TqyFyn`@Zb2nO zT$E9;tr#;IfT8wO#ub668aX2%SF@rTUEsN4^GAS#V&c@XH&(T58oe*_>&jn;3E!GMOHDRglXn3rzkE@+k6`^2=v}f(M z$`#pT`7ZUE1?X-vulj&6enMdArIn!^hZa-rEX=?$ZbuNCD4rIMOY!*z^6L3jF-uu7 z5%fsZe8)2tsmZ$bh4bXE8!5`mBU+!8MWapK+0O9orgWlov;P39^MwXBMLli&xnjd7 zS#nn#mk@SM9+FV6xVSXrs-&w>&MJ;uG6Rg(Vq!8%G#Vpxm)fY&ilT^q2^qQ%bEH*Nlx~d zbBStmDMLdP&oJ3tYU2C zo?)cR@ZUsC9JY*)9u;co#8wff?$VB3?9Pl;p}NK67_Wuo$r$G^9x~d>jS%jW?N}?a zi6eKsR!*PXaw#Bk4*YkbGc@qBxS8sMPZV}&Y)^|F;yo~{l&J*r%W0LZ{MoT^JmxW?S)UBaxer{*s zO0}wxjF50cIDoP4I}@y?cydWJ@e*8bbP%q+b635@$*R zgki_b@q7r8i!@|XlFXF}E9lQyK}!VI}HoWj#t zsauDv$<)(Um@1((RLjdIWfO`>BnpMd-t)X`FB)a8L9NAeAW+K^%I=OHsSrf7TgKCJB4h&99l9Y+sQ!kNzJ3h zdf62Z^PRwq)x!0q1r!}!0<+`H6NC#5!$=!$^QUGt-%#tBt}a5 z&uJD8#%(HC%n6kiSC<_!^Z{L&QD7!;K_pRv59QI=o(x#`j~t|78S>$D7PNxBRd>wE z;k6Uf-5C)5xH60pBXN97gDC3a3>k6@UL{P=edN0Y@iR)Qi=Fn6#T-r+n$adpoeFXA zXq%S9FxZg60p`h@B*@8Jd&SBwX3SdJg%;!*9a4B{pD#iUq~R)kz8so?UD>5$jjnN) zrE-WQ7}cj{#o8;2_s&b!wv8R|m045q7FhB*1$!pIqPSrv8-UKL%vSPm9q~$!tuYfv zRTN2SoN{?_*?dGV2(ks;GpuB1T{yyK05bwL3ng}c)X7KSDA24gE9*ivme5-sdg*o*Oe5j~6)9x#TZlHA)Ah#R`dUG+swRf)UCGkkpu(5Np}TQlrQdZ#V2x z-$ySLVn-e_WQwsWO}bB%Z)rSqe>Ojcle|PT48|1KWhx@DG}O{T?%+N$yH1^?#=$Z) zmww`H(gIGaF*2?MX-=h=W0jR3mmsQ)Pi7?kS#441s6|3Y`o7*}DtTm%tX_u3yn!*o zQN6s;b0n*5@nDUv6@*Zb5(( ztn%HJtIsSz2Mm&sxiYlOiR7`an)g|;#BCWaK5G;AT#8Q?`|K3uQU{{|u&$&4(?tsXFSSoeWHULfg;c~=oT^`+2Q)WuB9e)#SbB$UBQjpy z=U$tL#)wN$0BL_ZbpcCD6o*$YKjrE6I`&_ z0Ak-lukG2&RAh|Or66oxC3!p~-zlCq7ale>T!>pt;>u?vsEx;M<#2mC&E!8LCzP1f zTnoa7EUYPK^^m zLbGigwbj3Z6>XiD#ac!eiaPl6bk-QAIEgJ7-vqp)RZLs7c$(!7rRb3gF&W6Rq?Z_m zVrn?o&iyIcj?*TlY1~ut>{6@Vj^b+rG~{F}qe%))Sw#Z5eYQAkM+ZO325cCErY~#L z@^#c9+R$<}T74yWh*36fW>hn!LmGJ3QyIt6l{`wu)ClU@*8D*UN=CEB2IrlcT7lMK zQa`rWRe#vkqJ>m6Zv1>S2yzJDACb19V$4`p0hMRXGNk->eNwsQQ6k^eZ))4xiG)8h zELi3z>0#;Tw2>tj7yR~PU7>le&5=+MBnj$A3eCc#C=%FiPV{R%2ormt)REf2vXsYS zA0<(<=2qRyjEZK?Mvkdf!qMf&5$qydBSf6&vSfSc&rr%TycHl`C&5-^(m0p7O2>gh zL#IOu#^AmkV5w71GpfLcqFC=R(givtLl=GNy!=7gidV?Xu8C`cR7(2AB4}&Dt z-9={{b0H!rmds=(GU1nKBU)2q(rOZzrGmbxFYlzJD*T3@YFyc(dfs;*xSYar6Oi4i z@^VD=N{tm*(RFQ?cvg3Jt7i(lt(+mjO0XR#nxE+R%qrR z%vvT<=J=RX4kkkWnr-f(%QBeEZ&Ln7$k0yhK#+Dy*;yYG0PF!JcK-l>pm@EwYUP_& zVj~19n#Q7SgDk}U=)Cvb#^PdU55G}4_DGV9Om=Tp+L+m#gNm6t4FjzmYO%??;lm`m zgK`pz8FeE_)UDOT*?t0|h0qu4Z}^NL{VSPVV=hCiGkykp1rtPE+4+55YKbJK(`D=A zIgCbfJcQ+fs!) zBWDgwnGDWotW=(GcNq)g8{OlkD_Y`Ysy^UyWfoZ`Bc@|<$wcjGph}%@jHtKAHuf69in~3dwzXkCHOV+uHCygXBx3YnI!cRox_4Aj zt9ZC^Wy98I8o|@Pk8WyY_m2w4`rF2X#!K{=DN=)W1r=9YET>{AKxfC5pBo_ohQ+r9 zxBmd2(Y}(J;l~)>NwuM`uxjCV{{TB&Pa6b`f)q>niBPb|O-*+apK6V_RW<&^nMK=E zA5LaX&T5)Fy%V>L<)#*MC?_J*2B4@Ksm&Ymbv_FG1{|4VG$|;k^Hj3@nW6s1V`?B9 zYO}`EGSe{vxdn#CTE>*eZZNiZ+E;A|kt)kV$S5Tp*R_~NEoGdkOam&rON+8GW3aSa zB!kzk%lr~FWa#w>2Du_dC$|~kxNtW7emOOfjxl&8IWU8cF~{`?I|iKlUPXF;AtlF9 zqgwfkHp1d%?ieg2y^euK&aGE*5`lD?Ax_44J&JrE`jyPGVAbZ8QT@EMO{|$l8U)ZI zVGY_5&Q>R5IisS`sf{5K!m#^FOSv_L{{UmhHS|+*q1!-&rd=k*J6XP7jblvNr&T1j znK3f(j-l!PncsI+n=1?#D0-0(BrI#X?*@`>5h4S# zMrLQUKoQy;*(h-_1mj;P@ra0+o#<%ue4|>KTJLYCHi*-a-sU2$)TrW8aMhwFc9&3Q zS6vFU^}?@Uc6EF)R0xa{CmbV^ObwM?m?cg`txbLIq`@mziu$RF;-%u}w(~=n>6lsxve06RMA1 zLR8+JL76RZy6OQ_S7p;(m2V$VsTmCoT{RE!y>#S6F%Yld3(!ZD&!uNjMp=lQnAGlj zYG=hlsNaa$TOKDIEBuSpwEH5+qm^fko_*{YsG4f)R>Lr5bx?4uEBvGC9>Ow?E~Cen z41am}E+?p2Q=u^c{xcPmcQI0U=5c|InP`|qI`HgPj=2$D@_WdLpE~Mfb&-t8M8mYA z8>YeC-RYyyGnZ3E!3^b*@#MwBr96hY4k_iD2+EJeR2YoATei2Yh=QS2?PH>LXvr&Z zmPmu&TQge3SwfTC!Q#w8cU)kZhgmGq_?ogHMV#Z4H&zbw0oq`46cyD}>@&0_)rbB*hD|TCY3Mn1Ny(wce8Kh*Ehs@i5_q7Kv0~Uy}wBqbkA(jM89INAjve^8@cSC@4i z`*({}X?R3Vk%^S5>RIS4oith?q`28qR*^@LEyn<>F=HOU8Hn=KY^6vT0WWW%8X`@5 zSn-%PU0RzNt?>!jT0O{_&F$t=#`>V*QdPIU`mZtDQ3*iY7}EKIqtF7_Nst8ORL*6J zGneACfj+2!2Uv({&Ut3DXZabk80*xCoXa%jTZ1KT;LK$?dn0DyHtE4Be0UxSfeCdb zBs9rC5MQ5tO}X01M3^U*;=YKovsfBkkl7xXLn_x!K#IG%LPm2&SNH*#J3hZEm3k^O z9Dd-awoSo_QNn36t=xjqQ!?vf3`t7-VaB7TSt=@z_Zy;`L1}r|qNSlVJA0KLmcd%i z6&Sbe`3l7}3nc}+B&W<#S++*ebxzup)T3y4uOw?9MdwHM#I8kmRop%bV5<~j42trW zV#$;Fe3G(^Gl>aOO}6_fnVFdH-7PMCh3#=IqOE1wXJ-zq309WmnNy5GenJp4W}1u* zhNU)OuBB0tV@Fw!LO*y=F9)+eB6+JbY2*V+B^gRZ3CB4y7_yC+NLDy5)vKCnS}@Ni z-aJydKDi6X3Lmaow^63Tg=?4YJ>iFQ0z<0>R%|L3WoLiXoW@owIo-HqJ*Uc){{RUf zeJ276HacgFMe)o*yC;7pO^?A^ryfO_i%c!A6f4vkapJvn@^bQP(5d6n6-z8tdhN(8 zsKj8GYXH>oV01*`ilLJr7J|lr=P3n@w)Oy@@39Kh$60V><0^^4n8zQHC$pCkv#o>o zt&fF{J|k3U)S4uNO=(w{AxT)}&1~~=O^ox8fU4M_DpTbIoVYP&S3L2DA8t{}g7#YK z{&C+ZOXk+lv}vi#Ci*U6Gu}+3B7d@tZe}4wYg4^-A#F+aR7ZCu!&36OZB|98vK1&* z*?YW-gs65Z;r2gKPH2oFsQzJW#|ikC0N5!}6`XLUu}};et~E6o4@~5wMo5FF5K2!( ziTSqUe``ah8blL$G$_1Uw8a~fUN-E~D?v4?x+bMbo7Lv?1-v7wq1A;JL}Gm}aFUbC z=a!>+W1YTJk$7K8N`7C`>8YJ9WXN7!)?(I<)A1{kwMlTaV{1-zJM~S+xmCsKR|bqh zq|2OGIaQl2)Ck48qPd1 zjYoLbiknP|!3%?qU8LG65|viWP0>WsG^;G$Zp_Wv5%Xy;983hW6s<-9@)6mUGoTF9 zkEf3^$f7;)BdfvY3>FefChcC#s?+Hzo4pdI4WtpxOuan2GIvPtr>i zGHN`QNFT$cnZwg^qEoAVz$j=)`@N^0d^Ibp`c2hw{lbo_NZr^&nor+NEVve zd_wHRvHNtf44M!!L`*{QG)_##r7P=75!UMWzA@<;Q5d+GSaMdYaSKOt`A|f`5H!0w z&v>q(3RtUxs=5?ZogTv@jXP6f2pHrgCc&7{F0xhBNbU48TmD}zY?XvvJRPo$;ILO2 zmMg;D?i*oUkOrg7g%R4DT;iIqE-Krh40$f@-hzw<=5#3T-#X7+!UC?;R#aV>7asw% z4G`Wy@AUqj@?nLJ1}}tDG0hFqDZPG9IPxy|>g{5!q{m3+9^XL5?WdAXIf?9|y50M6 ztvt30+Fe9eqDqNJeidM;MUy!Sm0DRmxbuo3*oMKN)wK)}5ME`h3UMkOo(ft6W)CT= znOy3uVAq|(^<@cw22OvT^k-iPpOR&{<_yHwwxS?I(f~}cS*tA_D1|!Pb~B!+Vi~z8 zSnFITmcv#~zm;^cbyA#hlO8y=)>CCwGdnMVp%Y6Qr^=LfimS+pvPBiG>FO2KUucsW z@EcJS=V@J&YIY;?MU+H4pcN2oQZ`i8T?RunEl$sb-?@-~wTrR1;%czvRa>M~MPoG+ka za3`Z4$^n8)3A(qLPoqjQWyOb*oM*~Ojob?#HM-0am1uZJ)D~Wyk_Jp=7+N2Fse==^ z{+gE#y0a#=?~`%k>5a2ZgxZMM-j$pnCiiqNbw(gnAMwv-*tV;``beYcTR7eve&^}N zZZ#6aKhr0O;Oi~ZhEhqVlbo?40B0QIEashZp46`xpT)&?k8N*_)3Nno!_6Xwym#Hn zS4~<|2Q!n#aPcg!!t`>Z^-k$&8_ob7ZF#ZF#422m75aFkU+1 z*~<~T8})J3^No0!D9%wHZ#qGsBQIv+W!?+~{! z9Zf__XfxdR+&gH)v|S!8uj30S+CsUj__9YLZfRDDqB%4ZH#{k}+?R-Kf-Uz#*V)u`4p@FGn26HcYm^-LY2av~~z zQi)TEe82l^_{WdrJ#nM{T|ZfE`WpQU{nPuW=w7P6nfF88u}`%hW@|6nj$D4@d)w0Q z$K!g(7mm_VJNhrTc;4jo>1T9C_WE&(Yh6CK{+sPry!Y!1!Av}*ML zkT6d8{{VmPKbH2Y$A&?|7PNAv_pK`_8k;V~Bee>3Z{{EBp0n#7xyVjet$MGiddDVl zUOf3esm|qkhaouX^rFEn%d4`)Qr2Lg>KcW$*^b_W5fSn0rp%=~N>u*C~WbJxAUC zt;OQeds)rC-|f=6DYs1iC(N^F_WsKnoO6yYr_0N?5emr|{_$BY81HGv+J8d8W83D- zliRF^+#)PuRg&ooDX*K@?4(95=JS{DS&s6HF6a_`?&n7_3@={?BAbt z3%sc%RC2{uf;=M$3aW@qaP( zd2+d&nWH6piO+*B@0W%`s~j^Ad2MfGXtt`Q!P z?X39#f7^wVFbXu)Ze)79GEtGbRWPT`lgo4omjJ4EdeWw`+BU1=_?e$S>F+ITNDU`{ zOb$*=IU^tQ7L!-=6Uwo_ye+%rYEm)FMs-_j$FaT{l`8fl5Q#LAn$eLIhifvnSs8-k z`{}QOMpWS(tEH$lJ_TCzwdegH)ZE!ZAoh?}t;+4~g+xT8WDOXbLV_tu z+QL<0&qx^3)XC@E{{Tz}vvRUE7+gJXWfE2m5E;zS{T4 zr13q;?9X)jbKbs(>W7Zro$fDadXJ|2lhG?TFnZP=sp$8tIWIEk;46|+{{SoIh&syl z@z7B5tQm3nhcmreb@?ouj4!)TgVkNVb7vp5Sk_EOc(F?DXw=5+1XgEZadY_EI4RSW z<=*36~2MEcn4pUV1UlnbZc_L%wOnxq3j~BH1ktT7Rk}KU_RO;32 zd+JOqeZo5&VkQa6mgsd#aqr1x!K1P@9V%Om5vsbjmsT68M#mk(;E=!|94{nf`h64g zu9XC0P_#>rjf8?sO^SX{RXkINCOds!Z-zUVHIpnN;UrKxJbM{iwY+yt`ONx#Dxs09H)PS{$O1`Zfuc@|s_c{AgA83aU^~Buc47g; zNW#g}jLbj1>tYnUZQE7EC=Bur=VlZKbJWJVk&;zBeym;giv68;4q{`EJw~MUvHt+7 zGHRcoZ?pJd(;utPw~H(yYt%l zu8Hb>PgB(To~NnxJx^2WdY-4$^*v9i>Uy73)b-dq{+a2Df02(^1(itsIQ`0X+G|iq z_gCA0fE(dSQvU!rVn5&+@%7bzjH569hPEB`apefrL7pZ4Q^&u@FWt+vitL}l&T;;x zr@X?)CTlhOkCb)B)%=aVC21nN_+Jy8DLN$Rj#ZFyJS#gU=TF?I>!L%Fz^R*3pYqw! zN%EHlJ=2I?Wtw*$UkZGD`%hqn6?_aDO7-YjWy8o$C7UCKRrv`TUy9Qg5k0Cnov5{E zt^WZ1P`#h};J;`4D=QQFz59N})q>SaeU|(F1d1y#&@9w=kfW_UtG4Ufzkv*&);;9C z<9!^_T^NZmm)p_M{!fXHn0N35W;_#9FMgIUrC4aL1r2f>E`Nx<3b$*|(&JGTL^auX{NZ(vX@e!B$7wWRFCuO! zz7#o(G8K=_5pam?k6-!g{3x_RqYeKOwsa~it|;TA1E6sSWJ%!Tdy57 zelka@F-C{PlBr=N*RmPyG%RQR4Wk1{fNrf5(-+PFMXkK}lh(fM7^mJav zpINNhadud1$e22e2l&+=^KbF9{WfCj9z=3ste}5vs;?sXG*>umWQVF1VYnDh@_|7y zUQPqPgv`CDjhjMTqDlC)z=L@|ky()Akaa;|?PWl}x09f72JF}QBr6H{qxc3)2}2~5 zvaL(;n${*FPcgqC7CdCoYARAI#_%Q%UBe<(Ujjciqsw}g8vg(dDNuJljjPrYAOlIX zl(!>xXK?0L3e4H0&7}&LW}qy=bkr}?nK(&-^l%}k<1hTF#x1WNNpLc{GA#KBw4)_B^y!r?}uH6%FT>JsqV2V;WaUEn&fNc8%7|LK6Qd& z!7(+7v+btqEY#zyZewp0UfZ$z#QaK*hyl&G@(om$!kXqnczZiE{6$sc;Ka1U_h-nQ z!;ik_W(=r>j|M;-;f!lhz`qI>rn|YnjWt$vS(uC7N07?K@ybPG>2|)>>kjj)F&tDx zSFGT0yho~K(L%jrK>IOBUj<5~inEQ%@>o}|=Y88UWks*@Sh75klwsudx#gU{FuxuF zVvQ>jXNT4Al8@ArkRRz+R?Yi`(JJkzodL%#PCC zq(o}G1r+ir%KI6ln1SeW1#)fIUQC$*{lx00MXn8*E3@VYD}+TJA-2_ippredSSP8` zgOwxxD%Og^r*YkW&E9A=;DB&wTHrakyuw~VEQn=X1 zha!o{Y`e-4#6lq+@iG~Sux`EC?lg&%4b6sOGyH&OLk+ z>eeoaIKfKFZEZzV9@_Zr5f;>4FBqPBell!NQi-uto$yZ{g0xc6pBtRy4LkFI7-2RN+^8lD2(6HbSZ~I9m$y~p-se*=58fB$ zPWxcjXvVV1AYySV``2KM%1l60LRHfzG#T}l~0}BF31_~ z9)X)hJnOtkF3DASO3~heE=GhMTCxl%!+8|RQlLIj#>uvK2*=d+`IbcnHd&yuuEeh! zmNg?GN^!;S8z)LH9%xUeoZl)_solv@5Uaj1wYiH-9qWUY+tT~GY81@12%P3z*`7%y zm1@h#ci00(Y=AHb!+$KlOv%ki#mQ#nF&!aeb|S(n22O&#?8}`41}Ua-WLQjvSdZ@l zk&*Bl`h1ntO{+Yk@Z;)@jEH$9W_#TzLjxA%58TpC7D23AZAM(OZ=VP1+vo)1gMQV? z7Q2YuroENSG(il<((Iul$x#k$a`EJqsdb2rUn?g1QO1rp6AwW`BBMg|Y9S_Kc*&ahjAzU}ql3$N)K1Vp&Z# zTn5X9Qi#XmCb?H*XUn-yMqsTP)Z>DPg0H1@0=jr*%E(H`%kC-846TY8)n`y_ao6f< zc(jIUV7(O-Qg8x*J1qGM-AyY}GSO?J`rZqA(+7C_|A$G}JIw9CdR0F@LE!vEZL{1!NWR72=)o zaf?DVSCbiPfSqGJG^!#puIXWo*qOI`Oz)3j&TQWRhDF49N;KnAQ;jJ}vIN>d2?c8t zIZu#PlyySb65+J**q@j+#|X`JFwmc z26Pfw%@yJ&k%B&~>!f2}F}`z%RavIUtyGyjW`2tDB}wvjNVOKXcBvC)bwWU8rh^RS zK8%lZ6~wdCtp(Lcg=o6_RCJ&D%+G0`kQC`qryf0V3Re!V9x`_@;1gKlbuky2?35Ul zT~xD7fpOLh#~~JlZNC|+)1(|b1HD%P3oQZ6@Q8&*a^m*Uc+!lB_LER({{YLPOvDyo z;%w&5A(TDB-MsXsS8atHILv%O`ffaB*r#fitjPfhqSCZl2=cP8L0w}v49Ynj zfUrHKvQ8e_!H&jp%5{@< z7PRi!EQ`E8`(4slq9&| z(8J}ZPLAwLnU_)tZJWW1?1tlA;m&7?+q878-DCS`LW?2}#UDuy#zPEZHnYsE$K1u= zo%L|FG zxJG7T4Y>IjwYRE8DwLmvt`D|_?eOEpkerQUF*z2*TM*~Hr1?p-(%12PTAnOwq8|2X zG33HpvaMw7$`sWgh1qcKlMDl4U0=)=RbkJM8w$AN8usaEnz&oFEol~k^?wS@&kh~R zF=j8NIQn?-%Cu;{B7SG#Mf!{E;E9dxCV+l$fYoC-?h1IuX=<~HF{>hLD7$rHxcM64 zchI1ziG`zV$n@CX_fVp53w!S<;*;MHQnFFo@b#14DYyP8k zZRB0d$f(m`kMyg~T+2Q@FRuI#H^iaFAQ~IE{m}^Qm1~kHH;-7-)Y_JrQ6qe}sVDiqHz^*x%%z)V zlnd%sLUv^(yhRZ={*8$bCRc>aO|jU^fftk~h+T~)DJe-dt>~wgn~;(e1iijZbDKo7 zDiMa}Jd(&yJ8gp<{_8N=D27|JV05a@4EYEq$l)O_I0&t3P>^PzJ8!7@`WO+925HNN zrQgbViIN$*v4kjXHjXwr_(-t~KI7Y{oSaQ!WTP`0Rz2-yH4z)-tx($@;Yqy|P@tQt zEjzmsS)2itD!?Te)RajA12Y#_!s-J(_REVdG^JxuXwK=xEe=pzckwF`n8itNQ@FV! zAf%qf9T@NS?T$y6%&i#$4vg)%Fv<6?oWTnyF&vLGGi23T*sHNL20tXKlR9WyP#Kbh z7yPrw3d+~fjS?-|Emaby5|tZUEo+>RoRXQbh16MVL8IF#r;vz98?8?W*^IHO`US1( z^kwVpQuJP}-FuSCz}-AZUCw-zKC~~tZ;uOpDu0TjpnLZV&lDee`TUA*DOHQWgn^1BT z{{ZK}E&j;sxRH5LCew~%NN6E#z?#B5wF3R;j*&bxN)lMiWd5hO<|UZ!C3(zN(D^|% zf>Iln(TO$OuwUHF2Tc|)>@ZSbo4Qfq=|3ZLdC_>%l0b;U4hsJCG05fDE%yR!90HIh1|L0MAXjpgh_Z`{l30iF|Hk-)|u>6KS+YoOP5#Hbj4Mo38dMm26Bhxn%_CTug%B zS*C|3Y>jge`~H6r44R2a-EYZSQmNB~gB)a(#vFs(Mr*iVJe)%+D)>(&Q;$}ooKHfM zrs<%z;(?G5wWvlFtsR|s%PKKbO{~LZJ}1SC?X$BcT#=I{T&zyF>6OB;E|qK`ekdyO ziF3>-O@kWgN~tX?X3t60&hyBaLJ+mR=Az%~Uh zJd73^P?hpw^s<#_HYGPDANj3Pxj|8RMJ`SRBvVCnx^0|#%Q?EQCRNAp%8^P=lY*f} zwE&ip=64!bBCE2{NQy+gYZqBc(d$W|qm-hxU;f%<%Iw}lP*KO#WJ?|_n6cxxQ1IU@ zYCo!p;A>sqru66LMY^g$C#qI4dV051p^oN;l|tjtWc5gw{8UZgac*-}|> zo#A?w3aC_xC>F`^gu4xbVOP)VB|2{z2;pvBK~aTsMqwFQB+kO*s)*@ZmQqS&VaXU4 z)O1uzs7IgtE@?9{6Qm>)$3{&g)OC#7&StIH&>B+YA&}CC@b#oysiuo0MkcNO8NLB^ zcP|_m%Y{Yzp{M%|QR+0LHz`a6Y>2TcQzi$}N*+BHIM~*5akMR7Vtmvi!>KAN-I!)e z2)fkDR#zcrq|R2PpN?8`VD+xS*_Tk^Y|M0hLk4`9dRenh6FE_n<->6w9aJERTEHB@a|T$25?Da#nt0@7Gw5IJ$rV-+rH zV}un@DGW{iET(BY6Lg7CP>4dQ@_@25=&w08DGh%tXS{(C45|w2pZj+WHz@Ay$(JTL z$&sF)P_xbyNrX|a8+lr|k(g+);B-aCL~;R){Xq$nVkqvT@YPcIOS~&a);r>#MiJ*b zIX2|fdA$kl{ByII)fMVhkWvvJE$|wLFVy;~b6`M5Bhkk*Wy(*6xNtE40N(O(7bdx9 z8S6y0LdTI|`k%^`71V2cnSnUe)xe#pGyC9z{{R{FiIc1@BR31iy(uwy8d2S+q{DIl z0KEmS?5J7$J01T3);cT%Tyx(Y&Xn9`kxY2(-lNm#Yw!oe8TemgR*Sl13SLffbcQI93HQ9^P=| zSo7}6;s`A)zOof!@6a+imZ&wP6qYWW3V+h~3jVJ0ZG2?4ZzVS7aSmM^On3IuP zFSsz!H1N#t%jRqMo4XH)Kj4*GtmRemv2+DnsIVfv)EAt~W(SmAZ~`*-f4+*%d40(0 z^wU7<{C*(Bbjjmo5e6+aZ-?NnOf6$YtZhy|Z>gD$aHJG;VL7$J9B%`26Z6D1)p)y1 zTmoi>PU`H5wP+eqj_KAEPHM%AIM)s|>tCkjGm)&m<3}b=IYDC2!jh#+FwC3r6`QV- zPq8zN=UE8$^05f{R7;5*9Sfz=b5Gy2~;%rxo?f z7My(bC^p8J0}5tKuKxfwqH%e7Bu5{*m80W%s4skx8SSB!~oEN+T;e>Q$FfHfPyZP2{;y+P^G&oOrWB$Bgx*)@H(MGNI{9 znoO$^4`s(n-PzWoib}}j#H@$w1HiWrXHA+G3*$`kG~PqgBpOkHwsd)7&;SLWL@sAds|tu=JqUgC02Se=|_ zb0zlN^DA%+Lhl-=sj)VsFec|YtM0AG2*LQ}h(HOMF=C=qN0g11-~tNNw&hjzbcsp{ z%kALeREdVz_%^M%Vs&;iP~E(iw0$al!f}(`BL19A&hICUvK?;GDE5wL#wGx~+G>9* z%4-smqHNnLryZv27~CPEB9;bds96YB1)s~|$raQt7fs_$mXk6*mu)C~~qpH5s__}AK#5f`9G?PyK2!{EpQB*;#Fcw{pGF0iQ z2tTf~3T(zD)Z(qdTVWXDIGBC1l9uhK1!aY3Qtv0WQPV?98s1na3RXFzGyed@+o*h+ z@l7IBQ(H*_0G6#R^vCVSSj$&~S9Q$wS^!Q_G}RTmM9YUKPXO5-7eGOwp;l4daj3ADVsN{3S!J}Vh-Y0(^6vE|2!tbII^+K5(6 zeT1(yczvlSVCl2sVEItX0#B-hGS$Yi;oIM7QlqC5B3`3<(30hZXHpSCK_b$2uvyxs zlrpskAtn{zLn>v}1l1jv4AVJ(72mPQL=`wO>X`6W>I6ceR)lqPWkDxkr0aY3=_eAF zTr&WiEbP?G&nxm|?qX#B0A^M_YmAaoU$E>Ln?|AKL8eY|W>6%3YM!R1VrNqeBpuy`t-uu) z;}LkZoqZ;=RicQb&{~TTc`0dhYV)d+jW=1Ni%>dA4Nv2=1dV&IP+T>$qm=-=j0;Ro$1}S!lsJ zjF~}Wq76dmkFzT#GAD{XzBoAVeo+jUX{s&^BJ55m5IICXZYdH@6Umz{9BVeX8%<2s z4cYJ6h5HY5vFXX|R^^q1ZE7O{CYMB|DHcW*9C|b!Xup?Q6;qrvv~#fD;J5~3mr@mp z>Pbd;@s$s8M$AVMFXWS0F1NMTO!MEVDXYG3iZR|TDyLxi9mFLDXHt}k6%?!f;=K-$ zOgBn1#8JDKkm=c7z8HhKB`C}r$galNaN{;23*0~shGg5tCCA1OjTBj3lS*D(hoKX! zRUddvgcy^OEea>jDjFg9(d*=?3li*c(-(fIaC+UeBkSkY!F97!a-gcUC})8(`D z8L;6M<hiBjs5Rx-xvV8F_4)gPZCZot;Mr6KlCb#zs!{atX^e6p0?1rAJV~86?d( z#M8QQmhQ2=D&*A3lVqX3KCL!qB5oXqgn3Aay=axI`MnZgEavG}{{VcYGq;yug>Mg9 z)@FL4CSh3%S4+n0q+;_QP`;2HBh_ZfI>(Y_V_4;=-13bpAp0ny5li;d2G%<(9b-2W zs;U_&bi@Lx!KB7j?avNPnkb%GXPu_VpW6npGS^oZ6`mI-@D$RszVJ~TRiAYDxt$@S z9%YLr=OHG0qdK=t6$7X9xW_5>PLMly_+3kr+CHWJbN>L$7Z>f951Gur=@0bmeM$Xu z)IBSkWZ-f>)_XJ7cpOi6`oE-ljHRM_=e~XN{2nI;s1uPdNaS%j09Ab+tbXnP0A>FG z!iV@P_(R(tbB+(|AF6BIXW<4#?KpkB=$_u&6p_pG>Q7>cKP~?N8GgF|0Es_d$pbfA z^#1_U_lFF}V#%I)z9W`qZM|6vZ@HcK>hv!{^j~ZGC!>33+g_3BzJcw}Zu(cJdJmv_ z52J9n-ka$DiRhk@!K|HIi1YR{Lii$ zA|teV>NWoWRQIuNIbWlXxtxADZY%Nr@_QH6xk4dYH=RD;`u_m8xb)-$nu<=o=ww0w zpY2_SdmCP3^p*W{ALBp#SFSJhGap4y-qHU6#QN$b^~`$XJx{6Xdh9d)Fu%fQy8i%B z4d_37zg51Q>psQ%Awk6Te`|faXXSb~zCP=5n9GsI<@%1gay_B!E5xZ627agN5^V5) zX~%f_i}Bv;-$p&-!^0Bt_UdOA(x!#?Ho{{Td3P5p9mp4?ABi$;K1 zIi8~YcsCwA{C?ZoKXSU6chir{??3te{{Z=oe&gK_Zet!Pi~j)ZKl%RNx~+YT`vv-~ z{{SansIOI@rSpAf?gzfT%g>LmebD={?><$(@DFJEMfoz}x2=1R)Yg4`J{)1By+6{p zGd85SYsB>4A5R^YJhn`jeXc(IN%4sF#f*7<&Q9Et`Chwi`X{qLMgIWczx222KJMm| z?oYVB`}6O9EKe?8-SilFueN=c#pQB)45z;j(s*o~j&;39)ha2@ z;pJ!jLoU2Y{{Yla{{U700H67JXgGb2E%_z?0E$=t08{Iy2kPtfb@zkyrT2r@KJR_r z^^X2s`#2ue_OAk$PiuP%+R1uXgYBPe@M}is;!l0=ch4i6$Y*c?u`d_K_Jx^ZQ_IIf_FM#|?w$`|iI7?SI*CX>$G9?$1^)&-ETbX5#t> zwY?E9DDwTq?!R1lw7m=4{BvlbtuoZ5CB))Ok;=3oUJE{RjvidE{{TPZ>yX8TfX7?X)Qx<#`Nwt1Do7t_iljAcg8>ZfBECq(Q`ElNeYife_Sf{wz~`fIy{gtuC%Qi5*M&!JFRp#Zzo+tD z4-d1h+;2pcYr^AFbG7Ns=0+Tda$eWjoDnH<%l`nx{{YMX0M&Zqeb2jD=BJJ?{vY|j z`d+@;{{R)=;U}H%-~8u4QD3ajZu31S*yQp3XV^btJztN?^#1@^_l}gMIG(%rH;G|8 zdN;MUNY{<&3l30N;D3{K5YKmQP0eb?APx`Yin*de307l?L z{{RiY+Mlx=b1r>W|FPgB(To~N$&pY+XpFO}#& zp#K24Uh3va=6(D6k@uHB(%3s0ThToO*xznFy*nyAR4u>p-WRy0#aYy^9)H!<)NmiF zD4%d+=Au9NXV)|ObZY{i+_(P#x_x#m`s2NC^^epWP7gJ|{MzRq{3(V$vHSHH{g1p; z@Ns1S0OBXpy|K5r_VNB_{{V(Pe2@Bt`wRBt^*#1m?l;&^yIzhxZ`dDmeYeZ@@2+u| zrFpz2qfeFTUcKnti^hw>_ZO!6&l}VIL({n?Z~K04ro!4bnzxzzZ*iB}XT|O?D&)g9 zKmAJo02w`(`1pW_?#{{Z(({B!;J^%|dQ zd&}O@^uJ>I{bk7E`YrwI&Ev)X+PhLmXRM!%%q`RZ z0Ncm#asDg*7WTi!pQK@h`k(OVm+lNw**%xeaHYa9y|;ID@r8SiF2qk^{{Us5#Cd;1 z{b$^N3VZz2{Xg}FdTB?8yMy$Q?2-OrD~5G$eJ(s#uGsSA0T`HAH!AbB#aX!#nvcQB ztMT(JiWO|Cj}f!!2@DukD_X9mVVZKkEv~`2U0D2?g&z0OoR$f76}1{g z$G`^2lK3f#80Jo_xRg#0T`0_nA;+}NRbEKIG=joze9EV0P zC)Adz5ZrtOWdlJKc{d!LrU(A3O;`f=RE3s&N?8=tx$1nPiLW%8ItqWzHPrQb)O1c` zIeod9YDVHCQ!vp@lQN5}Y8bQBx~YUo%TabJIa8aor0p3FIsp)`SCcCubCle=ZmFkhc& z$1ueDJE=)D#K(P&n+wOQwJF`R9ZINV>Q)Nwz;&*h`PW;)AmuL^8Z@+AR~?5ERi~EX zWqn?1#)2mvhC>f7Of%ZX;vmg&#^Qb=3_b39R#ea1AP*%`F2r^UylY4)D=YNTUF%h3 zd%F!rv)vYLJVt2itpHk`WOzIvI06jm|9g?QtN6guLG=Ipv`LbhWMd^$hkBrrkCcqw=OcX(P4QKxiSox zqDP%*<~MG3^6wv28v2}uPBKjdX54nK7{wAIs*H776rv=J9U6W;bkF8(MvA2wk0h?L zjFe_I5e^q%*Lf19K#@=s#?LCHHAo0BjCCZXeTeBV@NjXB^{|#-m~e* zHnEvfVanBuoY)K+6+0>> z5~66FfrW=IIH@UU@q~=kg;=J%8vXH0PS`HqEF;`xBMf0VWu>k%S%{=XT$<7(@*#6d zK(RSCB@S(|r4))(ts6|J>f}!RX46y0S=qG?rw;mIQC+@=HaP0ou8uRIQYJ05qn^;^ z6EU=T&@Ux1DyJQp$B!J>v09%gWfUJXd7DI8qc2Hb7>GzU-Z4p0ilbd;o|E#lnO-?r z*{3SB>p<_RVm3!|hKd;n6lBdYju^PUxuTC_Ais98jvnqf=>CD}gP&kRk-c0sc zF=LIBc9!o~XzyZLvs1oLQ@1LE6TJBq>V3s$9GiCV)EC%sm?geC8lM*sOgp>+p^78U zW|Nx*$Wz>s1!zYeK^(gh%%M)ssfwr2fIYrUnW1&39F!%Aol35zVRLzZ5G!a!Pg2sd z%*hv+lw}NtRNmD=b~$(Sl$%6E%x#95ed`7x}7#4i2cO@&l#2X&u;-5r^QFlTVy*^FX&_eoHpb4Qw;IozobwZ{zn zPvOr=Q%_N73L}olsyfjex+`&!8wS2eh_;@uZAJaL5GS zRAq0cG>yXM!mjeA8AR`o;63dqodChBa8;d!k$3jn4brp=T z8@eL&M_L+niak|>D>{(4@Z4lqxQRaDsOiVLADwBJm!A$(OLldI3Qj7ejg-5_Tqh)E z>BdPj`0SziBQKo@l~|p%5T-Lea1_Mu>cIn~!%DJ~&Mbqr%mb)6u9f4cEVFiIlAjg) zr5REFiZL7DQd2%y;6trz;rCt;7duH08DV2NY_-1s0GUr+X!w@n2{zmb-62k^8pPC1 zLaN!=uV;D&m4_1tcCIC1%rP;?PwoOu3eB8A691 zS~BT^8OBa7p&;s+(}DwmDo;=znJb4qyUR~NZ1%zPj33eBr z6KmZ{wEq}4<0;t&RJY>##upFt^@OPQ#;lptsxw|p)W+T$sM$@D?wKfR7E>FA{d&7artaItUL1jLs4o_<=ulT9D>NNrr+3-%(G;a*~uR< zihRHH<`#GjS#9KbPq-u zRa#A3Vzd%T(bC(ror^t#uG#C;Y_6Y5fL&mt(}+dGq&>mRvezGkn%5P0qE=oKrcq{j z4VX%PobiT_DJX8L5L&*3nx4f-J=DD6jbx8Sd|#kR3YYFnM^pMAwbX>Si?0igi8bw(eOTBPe2+Cmc&~r0D3yaD=#;#PQu?e+P`i zh+>Blx16d1)fz#Nhb5;lYlA$s7L=ySGe*bG`lF#ZDl)Q-qT6jnwq-|Xza(O;r;p~Q z*yl zgFiWqG9UchA8)uN9EWW_lC^ z^29}UsC_4qScGvpGR@b@nhk+-6g4%NQ%VY@HB~<*E8D3JM64?|cOgb2l{>`JEpHcSh0boHa8*oN29o6!O9WutcY6La zCU>d{8tBw!Y$w91EA6v7=XC_1qv~S7@u^dp<12qB@2J#BW%=s0P-9j4iVJbci!8*P zbM6MR$|koDacj+Qe;*~Qc8JI(C{^Sv^=EfJAGMr~QmLF#3f5d;tYj%rLhIOdD*Gd> zg2KrXmNgNRC2tfa{NQLz&rVQ-sjSgn@y1z9#fCFuhmRar9NfAX$R|>GD&##)kMTWC zGW&7SS)taHDz%(o(=T2`=&a*V4SN@}kb0iQ7)5)wd@+HIWGNzHP>@7gqIW97xBQ^>Ri< zbFi$7q>IlePZlB;Uw(FL5|y zBvoY2S|y#5qGTFPtf6x{4D732*6|=R9`Gqk5&Z8x6D$E)pON_LEoC}0U?WZe^Kv?SqnPL?G0H|0vOKL4-iTcANoL*IO zwH7XzivIv4>#Beel!`&`H(DSpxs%~(ktXIx7z^o6&N)y|X@SN}Te_6e)N4~3qOw4h z42;t+PRdZep;67AJ}y_ao5q91oH0f`7_vrBanwaGh&MW#C`y*!c2vjhd28Nd(7=dC z9zEn6i2c@X%YsIGMRgOzc}*>qpxQFhNmX@|-;Bo3s>e!AIR(>NEqX)M;RmYD4~5p(!h%mQGSw6$8vwwQWOIBqSN zG*XVFE5oUNCd*5*FrqqeyuntfETaI~ODeXk7Yi)LWrclgpPjjdim@Xg>c1pn*OaPU zb6Y-vK$5a$#*ow%#jXN8DWf(f%4but!Yg>xJd~YCl{1q;q`MY;Y6}*2b)m$Zb*&Ty z89OUB0!Yj`vt$P(8)Him?n>M@9AlIaFSfXQbQKeFW|_6Q?Qh!)Q3wuZNYPS z?NL^ZYO{Lr=khmdEo1vdZdG>{X%LRqJ*8%}+1sr{Q{}%i=Czj$=0Mx|d8XHES~&@d zSYm_3V~bdk!ATHq^0NDUvSU*vN}X9pI3rRZm?h&jn^*kiB4D2ox`qt4E~3*mb!Uin zyHZU)Kwx?${AIFdrvCsPRAv{EU~tu^emaL6xlkN1L9KRc41vD#7`d|x2#h*4C^no{(C3g*%;h*Q5y1#M5t5BZOgx|PE}^Y zFM2$5ZsuB5IhZ?=i@jZrSXvaY>SN1P=yo0ae+Xpv=vO=-z4&b zSwA3Ql{YxdUC!!a(ayqS`_s3~cG$1Un1Pk~MuQS{imS2-o-tQqDqY(47zJE}QK@EW z?QA~UmQI1fW^x6WCl;+2f|)bnsU{39S)gMtku@oKC`mMlw;9wu7%?4)GrK9C!5BQC zneit`LJ_F7c~e{gYd2>fwZae-yI~fye-ZK(T^)&5pV7dg$vKflF3KZOFb1Y%^IgWl zhrrjyHc%&$ZlX-A5gTYkNK!Q@>Yh}eGvz(?6I`B_z)3bx8g*@~taco56ug|-n<&CO ziMDV)H+`gcW;|~)c_HRE5#pQ{_00wG$8hoURi^REaYSUSnI&Xvrm@nxUp?yVD;#Qk zZCG&;1@W1ree~l=*wIr|o*YM#uvhpBR16C5suuYe>|KwF@-5_@nazhLL&xGVC&*nb zEkHPv(B9Px005*+Z>Y5x;c4u|sP7&;zGe@Q*%eifvbMEo6&>G~{@-AX#so;guNkDwHxx$6t-^zZJ2S z<@etho^$A#=_N8mW%6IeC8}Sjaj+a{&ZerWpv{H`(=Mu9W&w`a+YkPq z7+0NXsgPh6h{sY`JWDd3t1?w%RUE%99~Ul)n^*isU~0pBOqD+W#dz{ks&2Zt!DIB>-cEwt^#F5%PKnps^3fz9ZsV!8hT{tB@{@# z8ryN=ttx*bAAe09Qz_5&b46sryrVFVp*igkturxc{id*ka?QlmFe!<3PUa=k0Z=UZ zgA>Rh#xm-{ZT#$42p7X>M#bbe=&7CK-cv009hA($@{P$+8mtvvNC!yv@<6i_KPqNF zwWI80-+hb;nUyltfK(t@m_e;3Q6y;5GPIsqKh|uhnhd7;hSN{0#Im`_*|r0>A@U`1 zy;hg|l^m_i_dU^{O2+_4RaoQO$b>;@#!E&20CuXU$vW#(JCPutu{yUv?2f@>$8>hk zS(Tn)l@yatlDp@oMzJX zWRA5mXQ|7Q)J(RGvhaUS=5hf>cAef%QJ4EC#PPW8VrbW^)_WG$kjHZLaWALEd-|3zT!LE zQ4`8gBNqPvgu2UJ!x7mx629SOq_D#VnNgz!qb9p2$tm|Zzap3rY+)Abvgbga;(zGW4sOQ%hXp zA^1+^gI++-1u9uYXr9xj4CaTCdvYdGZ*jRREYB~yn%-$AT9JU3GmEIHFGL6Yd8)H7gMCw3V*XVT7l#+$J=qRVOAOdkA?r zxQz*##P^Nv4$!CKd-r>LnpErU?pJ{!nAHVb(WtAd(HWWR5fZ?@*k8;Kh;#IoiGw)N z$&(p^NgAkDf{tSxwzb&>%clhEiGoZlXm2bDF<#r9$0Zf~uWFdzYU(Wqa%!^tbEiwv z6{w);SeN3_bejWEcHP;EwpU8%<8&^Uk0h)q))KFOD_4cBQ$A2?spSB?T28A@Or2z* z8I*X$>8Xfu9O7hZc+TeYqaI^-HQ-{mY)I2aWQ7D}{o%~}XUU+WN1`eGsu{te`rryaCa zo}7y;=g8TFL%;3aYcSI-HTgyk2m2{f#fibf51|rIOiEZz6xkF*bi9S)k&SS_I6o7` z)>4ER#%JiM=4L08h`&45W4Vv-tYS@dRkghXfc$`k`vwQ(davN_Em|XtL@~A#8&G^E z5M@Z@$QSbzYaTf9>^6_~iSgAA(rVN#jfPtZi0$ol5JHaXPZa4gb-ro9h+h^d*-P~$ z#;q!7Q&j!WYprDIMcU4UFYQ^u3PnHE3>#k|sbv^5#2%h3Svio6twL$dGMw2hA3RZG z5g(tMAGpU}ikZmJ_&Z1MUEksy8QoD`_4{MgH6~nTJxkq@qD|6mE=#+lQcf9aXz0HP zNtu?aetfmYGP0wC8dxygmvUDLGcSHbLO-%*g*Enq6pp?~nMbK!Tlhd%`jJ}d9~fJQ zDX!C5Et(ag&Rlh*RSOo6MOvsvB+y{UI)R99Wkd@~XDMW;1o=HzG(kI~YskxoKt*=xC+Dr}@l&}u2X(epNeXcC3 zDE)^n%s>-TH({NY7f7jDrFBr9J0BvE(%b@9k%JL=G=n3Tr4mwAMC7)$BC`QSZwWQfBL&)QBK!DJNRz6I{*^ylT0B@$py6FVcI0Dg085b5) z6Rtp>Or9gXn5piZk~YbXdU=XA3T0@;s-n|FGLW4>gJ_sD-aQ>~XOxwZ+OR7onP&NE zrqs3e*UzMY2WZ4!3$M~KXFW)WSk?@2W5N^&;L+ndlyxwHT3qOu(MmYfq%vd_#=Eb8 z;snHT<9OfCITg5xmp+$0ZY7F_2_-Ws>lmxhtl8Me1)3R_V7l$QIAXp7=+^P%oKKb< z)0SWbnN2A8a;)XcW639K*?vmQLY`PE(bBIdc#KMeYT6@n+IWr;SeS{KL*J3mCJrP7 zq*`-KJW-c-pr%NudZGkYR1Cx>>NZ9Z?W5e|mF?ks^EoF|*crA$GX2AjguO zY6Yy|epXw)kKWyv7-*EEapd%~YF4W-=-Do-mQ{6PxJDx3bn;G$4zIVkXs@6=TdpKwj2#idUkGaBqqnyLYA&CNmNA)cHuIOf= z1}BCyENTvA7T;-&Bv(t5>2&z$Ns2l-r7>w|knJ-P zKg&tUIRvgS=wm?>IMP8As|C#Z)$r=HD}^tr&Muw>S3)B%Px^g2%-T-3i3MI(q;fL~ z(}^q7gZybthCD|M+Z;`rP`NFR>uZ`5wZbJNXYbmb92ES>(~<1#PE8w8u)G-rhhohBo@8EQ=3&Kd1 zxXER2-8Imls|8Fq9ocGQ7g;W<(T%Li>?1rHuryhycU_0~-81xv#Mez@$zC#c#u2Mr4o(?Qwk06W-$`H)Nsm4f%F}#@6*TndV zLfxmbIXSp#veH~pk3-A9nn_=#YcA;|z@~WpsEpsYGY*mfT+p~;S$+m}RCXRR<_}RB zy&lY^UL50qlHg2gCKpu~E1cy#2C9(*+#GG z@tBhnn`mZeFo=-yu#pTe;|3~O_4N>xf0jBPT#3SS`-fA-;(lD~*3Edj|hCmWB* z&8RaOtYRFj8FF;uP$bl(>gGxcK1xcS^yVov zt5H47D>6s`$YW<$O3&sw1E^21)xQ4#&$Ird57v?WLVmG$pQq1idbIhU zZhuN#E`PN-?ZD(!xPGnoliW+d<^9R!@)1Z{ay={dUXSQnx-;?lzK80)RTJeIr+57W z+4McOPjMzf_e#bT+gP}fANPsc{{XPB1Fot1x4Dejjx(y_jY@^MnfSKtJ0wIwvY!?A zQkPyHFB6r|<#D(?9ycSK&F1nW#NqJxk*6b-%jI$?vg7f%+<2=!`7c*hL{M3of`w*p zu6Acpx%liD_Tpv~UKi%)sPqRHsox$|peLcCz_Yd5UR`owfUr^z3 z`Bvb1&A7j9r^cVEaVutzDL9tqam6Cd9$k}GJOchC_9i7pHluQX3ndK*uRqNhG5-Jr zRab9Z(ftqH&O7xE_?pBuHdW){t@qcKxcOH~ebwnd^rQXhw%6LR+2p+gh0OK8Ps9fF&37cSs;rx4DVBeV@O1IKe$OUOrsv31lB)_;zW)G; zh*RD>clDze@cvA=j%fElxxTIHPX*-i48QRMJ?grb z>mRB6lkXO6m5h^}y>sq0^rDe{KHTOc<2y8$`5t@P;*5AdlE;}TsHpho$j`)fGStmw zOj$1f05d;9na`EVoXs-!O(|4XohnW|Y_8R7CX`)5My6B(9E(#?$Rah7s=Fd$!{d7AMWl#(>7**6}3dyu5naW@srDt z&vM1mj?^>JvWVVJi?)bIG-IW`#*Q#ae}T6mqjilBo;OnrLFM&dtDOj>YYnE|?sQ(p ziHNmHo<#0Vpk#t^q+k!Ml1)9N$%-vaG6J5g0P_=~VV5ivW{RKN`Iz1{IkM#XFB!Al z$0<)m!ae&@^4xK`l> zh^>ZAQMm}qIvJvc$vfuCSC~kPJnF?manldU8SAAHlu#ARDv~w~Y|0y1DwD@4 zW3NsPYPm-FeT;;tlu~&8sD7JI)QHQGU{4L9Bg!pOW4*hal~UA?$`tX3&(jb%W|}}S zC|PvDa8IY6mq1v z2#}!VIi%0JOI6jpiA>tZNQqkcSNVxEGPk7J)_^Ta*Y@{?URQKlKXq6uf2rlLQYg7G zV5#8FRg#m`x40(9ag!b3!q@|}Pn5=*;YpiryOP*&;+~;uDUvQ0wLD5a=ej&%VJfMm zX5ZZVVmBM5E?Lwcz?1Fc@!Sc~M3fN2Rn^*L;dK75qVaY0*sL`HSin_@LzC}PaZH)x z5{@&?_9ZHC44g6;{#_7zUfa3RGcIJYY5U6Ds9S{wGsDR$l1a$XXHGnqu9zV-A?5_i zGmxy?hsj?r>G0JbRYYH=QZp6!ugH~tpHB1IvP%~|NLg0oB{;O%e zrs$%38~2#&EB^pgr=>gohyKNS!(DaYeyY8{>Aeh&Xyes>(f!_e)^FK}#TVOn(5_GJ z0N-f19i#YJWB&lsUiTO>WaH)7n6|=Q{&uET;x;x?jTZ^=Kg5B5C5I&I9deNb&&*M~ zQcqb#nb{g(`J=l(-&)?oUwXMQn~I$n^PD3RM!R$NsDBPrCLV z;B@0AQf@5{{Tq&_5DY`Bh=qS zjVnMAo|tWR<59nD5;E)vnG9W|8MeZs>fS?$V?_&nKiOEAi%X=-Rp3Etgp86zSfWjR zB64w9SAmYjSa8=ka%V?E-VuMa$zxuk@vdqvh^SVOq{NLB&v}=61u1{+$fSSSv(`_R zuHvKI;5Z7%s}c~hQ!7^B2vdqHk5|Nemjz&9Br%jzC$#*kq{c~!?GLvvr#qojJKY*J zULq30p;+GJV0H>BE-KUNwN^Zf26;bq7>pDu-dgCrs~kkEc*&DE;as%FC@m?!WRj_h zHz}#|*4^HN|_c-RJ60apLBZuV-_I8@cq2^aRdXx;Vy&tixT>~9lr@{LLCnupY8ax6tgRmjcNX2#crxGi&UzO*kGW~^nBy+CCLFzp`Ws$bq>JI52; zYHPjf49Ih7je0Khn*;lDUFIeMU^^LL2BL_AsoZR-x5k`*1j`0uj3cYlytHSHg`_2D z=ivyAF8$9rFDWC4Ag*yN(hfHpOT0>)mmJllYe%YfrY{4Wa~*5H>WiSa=p*e7z8S~s z3)oYuicQq8`ziPDPLP;AUlbq4JQvGmaJ{?uOu+q5+(0DacoI5?`D7erYPN-BycM7H ztIhEr#PNuJm4x9jRh%f|qIFEw-M9Y$sLyRksj2nQ@9WTgbPr#=`%mqUPWQLnKenFD z^>*a1r2Dtu{>}Essy7O&9%A};uX~HreJh&WOEf4Y$sBH7I@ry^zOAGG08$yUPCPlX zMPA)KE#Foe zos)y|Px@y+$1M>+_21Nxv7W_O?r*iLJ?T}Hlxg7|-$O5x{GJlMJS8p6*$At`(JENMeTjUbo z_k%+e<#o!8AHyG{Ta*Sg#9?>AGuV`e2lZEOa7Vv0P)K0q4x*sP27!i zA9B5?B5B&-vzmRmVa1peWGeY$%%p#1MnnEJ{bmL?j~*P6t3C~U_`uSID{^^2n)2hK zky`#2_5jS}jGETwZe|Z{`G}IM_g~=ky6@N4U!x!J;rdDa2mRW&-%qk0qxW~6?+$OX zy<6CJ<9p-YK85Z-K~GKf-&FKJQRg7z@O`1_^gP4K^iNL+{e=-JWPkQQTuFrp`^9n8cp5H0>z z{EVIxZBHtuit+;N$keIoPVMTXJwg4*cGg)PjQWtM&Offtrw)!xcFTbEmVud3602|B zW?hd;Ki8h=Tzlp*1q&}teNOAZS5rj?8LUp&LNzHUJ@$5Q*8Bdb6SwFa?FXF7x9ach z>ndwTqALB9`}m-m(NsVCZK!PcZ0+#Jxc>kGkv@@AwE2-=a6ysrvKpUv+GtzMErDL?ft*&$NvCdZ{6#Z{T4U+x9PEJ z{b9@Y_m2MnkFV++&AT8#8o#wbj^RR>?l|F*p;I-*U0srT7dmKJ+=cLRXG$C_CjJwn z-fuoN#@&I+l2L88C2F#LOvbV1ZdOSf{{S0nn~aj{y=qP6aT|v!49ZxO3+PdG2=l5H zi0#?vX!52|wEqC1(6sd}@uLOVZ#>Lo5$+d8(fgsrxfjf#5QmqMU~`KxXU;-#6uA{Y zHcDcgq8ZamB_VzfvDKo=Jsl_?jE0FtpJEjX^rI*XO+&ZsRl+i&nE~ULW3uQ6)B01B zCSjK$>S=s}R{QvklU_Z^-qArzk`i&-d<0OpuH^n@K=V%1fY zUW#A1k7&|aBC@6ximr*FaEY``>OZFU7it%Ii1fI+gpfz-v(3?Lij*c?c`~U*OW~hd zs{a5C8QX19bov(vr895k!?F}Z72)0PIam5tsoxOI94MHmD8^&%R3pBii!S@pVOtcM z9`j!6w?fRRfpsfoLF?FF?%T+T!7!)x-!mui>vu>jOGCWQKXBvRRz6Ks?l~zYV$s2Omy%7ZCdkTqe{P`h zfM`0+;o1T#N@}P%EmKw{gcVX?hxyS+E}e>gvxZv9yp+UclK0%F0~{DD;(IwccQGo+ zgQ!rbnchrue6f<$gst~xBBrL>MrH&-ze*swOswSGY^SLKY865m72@^ksFJ<`13iSW z7#k`ma>X;o@?{h#b zlNU}lcHTWi)Eo*&ZmiYTF!P3T7DI($)-{h~*I6!JtCNUs4fkccSO6)00av(unS~gInZt6(s>R?qDWG zP8Y((iBMcD_xT$y(5&#ivK#(lE%DX8^tAiUi}*Ztv3BR?BWY&76^IkCsx+WdOy~HR}1mcX5{Y(7KYgGG(E-WQ{m3vcOvvMaYBc>*|P*KF) zXPHtc7HkZh{3xR2vpVd}R@I9KWt^6bD9OW^kv2Dy%F;UhHC zPu!I-rx?b$IpUIWO^b4I;~GTj-)SbRgO?^!@@iGJ^@{jvVUxxZ5Rm}zg%v6y%7fSf zaj4Oe%?gKI{s`G#er(?<9E-{P#!Mz<{{ZgAh3dxABs_9-lB~Ly z#^c)1CyvLXr8TT7j_Fb~3a;om@qzLV{Z2C@ zU+RIGd2;3UgC;etmPSn@qiV)E^H0P%lgsTU=YFCP4KQOLJ7gFi5|tq;>TW^hI&sIc z-E+zc6z{zImMmuYm}n@1%1RmNUFk)eX9e>;d{iqjPDojG86h<&+GUbE2i|^>fD&|* zt#0_VtRbD?MqHUI)l!m@{3EtkIUvvEB)&G-hpSd&lM$_{3T;@P+~FQ? zCAZ%3B}?zyV$kH{bnBO5g>GEwW;8?$%8!If6?RG^gUSt%9Br|Ej^%6GJ!Xs|**&gG z*)-|*?MJ(p7j*n)ct|}Y+`}$N1Dc$0CT2IN*(KA0ki~<7J8m@*TWdI?@s+M7umyQn zlM0r>EK9y=C3c$x;{O0Sa;s7O&Z{evE_j2>8JkZcd2Leq%E_uOW?C^Qj?tpM2WZ%u zteEILRPupYF#=SEd8OwHww2Q}k_ytVArg>DmS4!!S=V|h#SUL(ENb*>fLVzkF0Ok+ zu6#L8Jcz(ZqKC*NnQXucrZy9+WYfRZ_><_dH->kbCmhVuwy};{Zf(X6G9or(OX_|3 zK)W@9GA#J@sCcU_dx~i#?W0j$b6KKH&rm=t?#!D~NclMN%HJ$_AzO=Ut8s9bK`A6Ll;FUDy!m~~qjRt#3f z-n3vaXZeltHf?&@)P6G}5E;Rdbmt~aoVI*!ARas2#KcDhi@{fBw}8a5tXU0GpKiyN z<@SgNP6~xS8?ogwp~qj>cq{+nW>O4x=$;@nq2B8;~`=P!gDR@2rgKP(j1)Cmex>7OIOb(7RiU?7O+HN5xMo&fybL z8a0fl!J533ts9Q9Nra-#;h9|Q*jHJ(Q$Htv?CRDv8LXmEEg79>FN;ts`&JO9nI$FcinU+y=U%uHtP`#0BhoIWD!a0&c^nnB0~Lb2FFV+jD8Z4XYJD} zLb#PWDMr)2HY<~#h3y^}oVc=PO-HcTVW%og&lxG9jSLH#YrIztP;L!>uOcsh@1+oe z?y^#PGTl^p%8H`2xWM?w&(6Gr zvn5wJmsM*S{0Spk&=hlyncQ5X83NG3d@w?*fS&Xj z%k4vdIRsm#7Hfn?)e8RIZPb`g+aFZN&y{gJJ7dXikYu3O*}IE3u_V!OpM`7O%mpV4 z9l(jyVDT~|fu$@jh=85m3T)W&C3Bjo>N^2=N*v_Rk{>vv&=y_c)}&3nNE>8C!yFm; z6ne+G)T^zQol<%K1EIFG5UHSZPYSvJ;!-|!@vT>tLZN7Yadh^nC>f#-P2=6Moo2~t zUPkL+Hiu)<@4{z){W-b((LJz`cWqWek?_sIThr%q6OKtBkV&%^)8pJ0^-q&vebAI84OwmOsnBImm%T>>3qpYGy(UpvAoG>kP z@@6;WRjqkN!D$c3#sHh7TM=q#C=6PBNiK&ss#Ra(>z4!Z6ohg?*>L~YxHSRsPtBg_ za#Bt8=qDZy<+e_e`gPCiv=vTvO>sV>1y+D*-2(}TJwFewc3o|RCzXPeSY~#ddRBX@c*PlRf*F-34krn!v3~ufQ8_;goBELgR*&m;!Qprd!Oul5^ zOsYr-HTLupz5?m)FE_#_!~am&#s-mKk6E9zAe z(w99@-F~)Oil9FpsE|WwfWP=Fwqis(ARJ<@t@zz;sThSAJiU8ed$G#R;EZ@vbIslsD3kc(Dl!FdpLiSL z$PMcgMZs}BX5M)J_!Jpa*abNco>uj*`5lL_A_UaFof)l~Dc2*DAThRl~OgK>y$@15xF_lA7j zhv&SBYBIlgsw(k${ZqH|eVhajanDs~bd46oNxQfNHR~gKxH0Jwh-PRR>kXwuhpV?Q# zeUp}1$bzzty)`Wx!|3ircy_}mPv_)T}W1Md! z%isyLg=_6{1Q*VbUOwiZf}_kKW3u<*|W;i)u9$H*3=M!EAPSzi)4*o7gq7#mjg# z|Lmz`yGPJl4hNrllG@Q*#;;x-;a{a~%I?qmKDQ!*slLXXn&n)p}l>G?6k@8d8KO26Q;&bejtS0mZY@r=Uir^Wl%4Vx3b zZ4jUq_a~*QpX1!S+7?mvr%g@bQwQS6r@-7m%Sfu}rz8To5twJlr4`EW-gd~Lg4Af`c4HLDeah={0 z&ymNotn>FBWRVjba+&T>xW&hUnT{a(QlfQS`@f6V)$&OaC+#S?<*>u69Em=1byW)F zPgl$?5uStISbGPOO&F1w5V1cE$ui-U2GG&|Opys;kU>DmO_!e!W(;F3_!au2t=RZ@Y10LPEF(EC+~E>q1D2+1_GNWkFc&&aMwJwu678aDlU#4N#&P;zO!%RT z`yf^mA**-Dc_fnoFgRO3IdZr-gnY1%q?0~(qnIcJMxU{E-_{Ye?};* z#bmi|5V}bhe&5_?H2Y`Hhz}LKp3WD-HJ?6)(rL3Z8IA|p)wE)6mFr=de@~t4$C85N z!A@zgrY$8eGv zFjZ|0Le+%y@DGZ~Ku1w22PFsG`hTA6AOy10*tn1D5Hg9TcuYT>){_>ito24h!k2cJb^K93$^*qg9N=7ObS!=IPm{&i-Q^6{QX3|lkopT z0X#)pVj61+xy%+$?>1A-Mh^3^Wz0%&EWMmXKCY?w(gS|Z*C(oSG)uE+@Y#c`b{I-# zO%!XNVsTeM%GJf_M^^th%8_@z%rgCf&d+YzWG{1Y%p7~gv>ml_yWPV#kG*vln-4gx zS-0=%olr@kdMj!Mb>UIJx~)qZq(w-@Q&e*+gxeU3Y=r+#L}09jP4t%B7t%M%*(7^X zc=5bX@WGC!_tvOu1pj2Q(2WEy8`SMYrJhpg3aKneyZ*})g5!1*e#Zr3T(}poXU5u% zmmpSz)!DtHs6}pF=X6BHZmrn(@$@m7e^L!m4>O$#Y#Ga}pRA z*PffHj88~lO?J>NTTPQ z#Ko)_rfaIdO@;ybO_#uucK6K|-8R9m*ifKYE2vBmCLi=2v8J1RPI}m@mOx)cLj+LL zwbA>;OrG?FWv%%n5nV`nxH(4JE_goBYAZLtv#3*7q4Ae030q@Rz>Cdvv@qqd4465l zCbK)@=;-X}uQ1!VH7c>DPTwlg#8s&2A+L~`Xq+=KK|i%Fi-SvMN3ii9iaeX_cDA59 zn^pIg4)jlH0$GNw3UiFti8@*gH=CcRGFwdrq*^ zo^;R6>dKL7?~5t_zsf0h!Ho`ZeC9l?ExtFnN^mw?hB&AsAUUn}_GEZG2&||*z%PQm z2YU#5ZDQ;&&tP@!oOledn(++G4!2N*qT!Sm2PE3(@QtBTdg`=8PA2IhA?`Z7lJV@0 zpT~d=ggLhvLkMZ|Sl7_1EcYLDmZgoEahDl(Lii)6a@S0J1uRK81RmR&URu`vMf*&B z!$L)+voXPVfuvq7J!G>x@8Fu_U3RIF@cfA?V-Ka`K$bPBk=B}*>A1Nn;6O7H~ zvTA=FWp#MQxc9=cfV2!%>W@Y3d7A|hn}lKDkZ7W!seA8EL@6j#U$=b!eNGHd%~EMf zAVQgrFc$gix`AFL0@Ab&P28XOi@LX1un!l*Y}#P%OCx2fmM${~Z<{yM-(B>n{oEUe zo;tJ#n-J^#$pE`m)DN;+{%~eKA2g&J)|;iJ^QD^hrP0I~_kx*S#xEaxh~^w?Bjp_7>2FixpFK=zUG>WiS0+asiKwB*Df)`xM230Gr+1%gOqd@#rr#QSg-Dr zLwt}lAW(;#!u{=L>@?NK_p2(!&WnjDIq$NQOKnQZ_|5I4PV52 zk(+EQr(sCt2g>wT9ZKWLkcO1*rJw3?u-h(k;YgO1QfV(BpHGcvY=mlbGXfwQ6OXzT z3XNVq_Kz2}KWvp6W@0>_qn0t2vB|>vCQg{T)|AWtr->9$P+DMXTZvr~f1>?)nrEA6 zLZiu^pB$pv@b6BAQq)(HR*Uw!jH%>X`tXA6$~!!St{f~u0BBE19h2Zpyb4R=P8kPr zwhtJBtWt}H?pu{}fsMIZ(k9Bx?(o;i&ux%dR@Xt(3YTwGX>as?nUV#vSz6HIXTG%v zQL|$e_LHTh>D5A)F~FY~p^oOZj1W_q>vdCsucwn-lq>;@g>y9_n-F@m&T(|2FV#X= zwqz$FOAx~ehA$337`UqBy|e}7K5T&v$OYG1&n-!Cc~Y`<`Q^^ni<(SI*B8Kq>z z&j2-nSym=>4!I(>+LDim0s+SC zxGRB8+LBsD3+`m@-dl=`YHkzmuM+l30*lFba_8|2Ec$QjFBuL9L3+9qy`p){$1EcS zojs&S>YtR&Rqoly#4cjXbC=MS{7GYwlq*WSN?fw|xfp5>T8%AwORjBtsCTKR=skD| z6@O!cY3Ye2Svem{Ul}@Lm{iV_TvRyW#G;@U55LZ=u5(@;?bkIr;)63aKe>gy_c%_ z9}8^}4GdKWOxni39y!O+aJHRKAr8;z>ys3Li5tFa9YM% zf#?2m%cQpjf#$hzbrFr0YmCwC;cHH|_9>e*HWtN#Tb^TEVG9wQ8H}TH{trT*8FM`0 zDF~qErVWmkSN-~6C*-G1>4ntbP&gpf*W_CYIeT@-E5xn>9=7kFaML&%~Ll{sfeJs#jxe%BwBfBABn{JsAZ zIHBWWH}2vLpZP$>O%KPnZ5P8Ex1`0>kgge*a3;3z7_px;lK3kl=Py}oWxm2YFJlmU zs+`RrL9A=I8*{w2;IYrwV~2}Z#>3?LNeM#=SJdW%X|7vRf2O%-BXd$0bMGB2TvqpL zWTb%>Jm_!}b`h_SmCm+@L@`kaARaTfA-9)V9@-l^y1!NY5x(L0iC6QA`So|Zz@oN) zJN>RvFD^;jM`|il>y*;f@=a|_sa!!vu&$rUz~E1R&hMn3vY(%84mz$x9zq`z4orSN zse8DKWpoeS7`M(VTncvYKPHWAJ<88KYyY@^pWRJzz2;s1CVlilEKdYARRs3P|BNRQ z)f0ydJp_gaa6^_YCK~qA1U@43HZ*8ukm#8h82)1Xn@;AWS!8Y~SSJ`3jPX)-ru270 z`VFDDAJkO&CIOGp>imVy3M;Q4nSS~Y<-uqxu<#lEVgHbLPEgF`{8{_)aZXH^q8(5? zbCS2zqj{ZRjy#|xm7HYu(~;+)eN=k(yS1hP4^=`|^)9i?W*hR8c!`_InvCa+ka)dy zdL(Mkc>!!%ncM8Qsr>XDly@(I#P7k?S3VkD#lLxoXnpDP&sq)+2;>RyplTIQIJvr} za`fMgdz~yu;cW3(vqD+yK~ERFFCM?>t8Ux_vy2Toqz&Sc$$NxV34jknN3vWw~MH2;=I%nz*0Ye6PDU+SJw zXP(Dzc_(8I*Tx=M4l(<$)$N}U_4}SNBm(W?UgBc^#bV~zQ>_2N{2B0Y zhM9Hyy%=%j+_vt~B_0ADc+_xY+P(;EI3XYE3dn1-Z@|`LzLEbOeO0bEFMtGfRxCbp z6ua%6io+lOL%ET-tGV+v?YK>8rZ5TieOZ6bdtpLGQjhu{@Sfj3AN_|yOXc0AJR^pm zGkKL~_24Y}u&{d8CBBsaoEOXuUn{e_I1pcxozFeET|hERsgNJA2`j{ej5_3FQ|m8G zwpTtn2#C$zkF>ye*W+)*B4Tx+j%AEW6^^()l?v7beg(@ zzjO>@>q30*`QkCsN7UCXPL(0^qG z)^q!$c#U9)STsNR4`osNsPOjxgZ=3Kf7y?lZvQX)@qZ|e-ShvUP)V4jUkg2yKf2!7 z=dM)@-PE*BRy{;?J*!W#koZLuJ!&uK#*JUEYwWtV1>}BYG!h5Z^lkq8s?^s&k)WTM za-nO|@nrBM54rnt7kvG3?jJ5>W&0afge4QJrXV?DWRjsb> z`!(ptFA*@@`he}wHmuPAbkmxwz}rXt7m?fKiiZJzq=JA#&;`j1SWgm?xP1=%h(t}M z|1)h2>6w z9{5?@TU%MY90#?6o_wFIlA$k*w^_k0zz}5RvK;=L+vf9v@q*8-|MKZ3Q{riW^SL6u zhU)M?lcE+*Fn+ z7;0tgzsp)>`42_3OZz_*TX3Fa)*bnsG_NL7rTl0?wZS6sFlfCn9_%8~ulEbbmvj((P(+zs&}+?M(o014*`Cy0W3=~LwTq(+t%rCSOq%A%658YC~))+n6Bw;H}i7;`RGi$j5@IZ}2ecO4+5D$HjBq4Q%-ZHk!V)-bk4`Ty-a& z!~v3U{-}Gs_aouypzC~OJF2QpEmOYk?j?`Jb*)0pefjb03Bo?>33=`$kPo_+YVsBT zh*RPqrsLqt`{3j2Nxi#NWcoz(V@p7=eGp7SFK9WUEU>@vZmr{H;W)3x#X1Oe`8n1% zDD`wQ*v0n?r@*?T)I!vOr!=Q51N+#=BaEc0c#$>#1Kq(e*TnCEV z_UYVbcX`YIp$I%6H~cTA{lOWWmt@^(-8;`LFG*r605_ET z8LD#g%6`&bu@>Lv8T}NlPWV`8414)=c2v7DDCkOIUoUQY{`l~?eqfS!)axISNOLer z{W(awDCNxs0p%!;#D!gk#6!=*n^u9w_8czSCF)olRs3PmkEcej02LD5{g0GLxYQ(OW#5@gNNr_Tl_y13`o}U$WA~)U|-8!57m8F;PwN?vwXsw^??Kuz`6!& z{Ibv$yqs5e8$lc^IGu;O-o3Pw_xV;vP?WsoIi0Y}|N9}~7M)qpSo#O=k4&&W(;?IT z$vc6&^y|sZLW`3g)zNu^kcYKNUeDUF6n8CGt9R5sV8UIHDJ)p>*(Mk_DC$AtFdpjm zk{{g`{hlx53svi5M)BZL1@bkfk~k(|GG`deLI~O>RYLH4=<)cqH}E#$%RwVW(#aj|@RX675GvvKSmL@0|ocyy&R~EqxtR_RcU)Y_frbohgEGReY!UaUa z0_Vr#fjC%3`SvQ6or$|g$^&i;A8bUZAK&2{D(({niO%h924TK?3jN;dEGE^y-}x9+ zw1wBz)6Tda!!7Ridtj#zu#^PcH>*7T(SRr&(ls*BAwTRh$9~bW(_MEbI`hpqan?7V zWj9jCu$CCt$f=eDq@5HnGTMH)vO-B!u>fIA!d-v z6nm6Pe#k&lh&9sHiK24XD~VYAkaS6{a+6~d^GY0;{Fgi-SCq+5O$AqEZ!fUJRQCv_ z_T7Yjz_zEFN?KXQcnoc%!faar7s9}aw`>LOW{HH7hJ;YVS-E;)v`S{`aPV}dahUuB zy>|_XFn)nnu}QY;XI9!FZC*nnp`zaSh9m;(g>&G$-WI5%{7eOaK+fR6G6sGcF=9wQ z?OGJWT0u--SASbb_KiIuwtPvIeXjTfNEQ6bEwd>p*Fx9Zi32z7_=Ui|>z1m;H4vj2 z!(J|zM0N~ND3G)$Y2kidfpk%Piz8~iQUC5(stfQ8r~%bW3UTfJu=2D2@Q34-@JZ<( z!b+`EGjR@${0vu|aR!n(+=oHJtz;#zIbgj-xIN}QNSB%m+mp?h*R)8jy@Iz4{v))H zU#{F*Z*CT(-CZIgL2T8+yyox+ksbFr*XYqTW9xf9<*R^!#6#_ogXm%gDOGF+MaZK^ zsOTaYoHq{5qfEjOs$>n5gVrE&(V&T81;CB?l)(5^s$Ppb4Y4~EP&bsVCE=1O zT*MIm!qy?jWT25MqG`DoYM|YPjVoOsKpE;y2~!Z^W|&QzLJr@>h{Y?}3>aOXe&GR{ zV*yvk*^>V{*Yvh|3J5~5)txl^(Kq$CziCQP6q)quo0xv$WLh#S45RL{XPNzc-YslY zMTyleJQjbT;&gbxD^lMI3=r&{Vx@*heIA*W8*-jrjVAJBwKiMw(+FD^SJl?~p*faV zvnSRsn>nD9c9uamA-tTkSY+DQlJ#)G5|)IqQ}Bnq2FB zwaTjXAaO7NrY*iyjbw(y7&_Ywif}7?`E)=ZaqQ&_+5mY5JfZD_78>=kDi98vUXi5U z)nqKH*I6SmO=egtsal(il~H^Q8mi^tTDAmsH0sX^W^u8V>BzrNB#4OjI)PqkTW7z8 zRq2Z|w4mfHjm|-k%Nx7IXo{6rP&7vqYnJOUx~Y zhD#Z5`G%O06&%dbTW4pp%o=E&xBIUtrdCKN`R8S+WZ>mm22cK71rm4yT^B!>xsYYff3=D7BFRq9&nvDzqOt)&xXYQ-s_Nba z`iSs0Eq@6W%DvP<=_17G7%WLKE|bXu5ZCnssR7G?@#1jTv1F^Z=tISDq3Y{d_P#7e z^J>Val%A~96z5ESnYBG%%eC0nJNK;gH|qqZCjdrI5W9|w=OoB+Pcr$>n3bAng|4-O z@-0O`G$EF1PKvsf*?!W%4irA`Gke}6+*RTyM@umb!)zFOI$)YJPerid9Xk92t_h!*TP?zH%2IaxE|lN`8VL6=y_jh%3YsawP<+V$#;&}#A_9wfE7!4^Xs@+>Q4ik=*7YGi@wGIvZ(?PRx9n$A>GD^eb z5#rlrOFsFNuSQb-or_N+e{b=w@0nmYdy(M?TX1jB<}1hOAWx2}s#8@NK*v_slY3+Q zq~R6GJhSD1H=O;44kPXBhq^WXX%&S;>-t+OgYPxeuE$}*LtAzYu#=r-uvDtA-J+S{ zn%~B^sgJz2JCWHL1z3h9I;I1s%Uhdi5~RPkqOCxzv;#b7u|IT_`~u~E#;$Zcu_Kka z-V;L`T3JpzKkL5@{LEyrR-4dPi^P1JT3S8``q6Up+x{wJoMcn&wXTtA+TZ>6M*Kd@;l(s>x#c?XMS-cnK) z%9)}LgKl*ZxRxhG+iq1>UYK)(!vyOnHg59)T9y@SQsm_U&m68+@Km7>1 z*CkM)r*^EADcO2T5K_KflGEmLkmj-&?i#Qj%9oQRGft66l2zmq=MI#nqS7>9!atZt zJHA(98&{RJ`Ag}L15me6O>ZmmDR`T@JluxVQH3d4TJGk3E9OZKW;x$4{UB+coe}>TTJ#@^5C3r}+Ibn_l3NA3*xMxL385-J7x?5wlL-kb)ChROisUeI!vp7GK7os1Fd4GBurCJV1peyRlou|jY5nWuq(9{*ORr5{N ztL&S>NI9r_)9sOAy%;0_(2tF^W*cE9N61#;()7p4GjlXCqY9Lzu4#)Q9vxsJH)eT$85}jDU59%4L5cu^uftks(JR?xDIb;w`OvX^LURx-ww4$XFGd7lW_8oF)i#J61 zOi(PE4DqdDpEVelkEux~y7Aa;E(Hr+1^Bk}3{ZG0jl)&Z@s2WD3=GSEV2DVhn{V;F z2#_;a<%x#wC)g`@s4XNxF=5XABwGP~O&pF~n!sq`7)Bu7Lrg4ZQkT5}_mgD3AF%2s zBsL46A;@FT0WzsRJ}fgBj@X%#1c2Upnw4_P>8?T}=C(%CAAabPEHxhU@f3D`V87Tg z3+svvV)TxaUh!Hp#~cwNr63l87zi`O_*@Jx7LlOE{KYy@GWbx5Akr%R`$ji##B4Sz zn4QQBLD9TxP`|GHnG}u#9uPYwwXRRI*06ex^CsHlHmJKI*>{gM2gUWXxnu1GBk9V{vRR+u4nf}* zqES--b*I`ll%S_r68hEzc%^N+X|U@qVkjcUovyP|+lzS?%v;xDOvYpqgY-CB@Aw{A zEf3FFl0GJe(K7UDs}Gb|cQqL7jV(!D&UKQZN1Y zCt0m!&B+V|!?2iw`qkp9{y5QBlPgs&;AV9{@0*yFSSVLzU8xMi#XUB6!|Z4kk0XVV z%p@EX3F#+^wTMzDKVh_D47Zd+&??RL4n2&>DdHqe8QK=EGB)pI3tDh}{yc~=&Iu2> zX>ZmomnE_())91E{Kj#7p|vda*z>BC$ykZIyIk(Ic1kusBnKz8Bmm=U__0Ao?ar+wBahx(x(863(yX&qUAdYK0DCbp2!os0OP6ubZX8(qBJD+~__~?OId!J^wUjkwB%F zky<$w$p_Y)F}dHqOyvpGM|rEt>_i%&Ji>Q{7Ds?D4Hk?|nTK=`g!(hjeUl+6WF+k4 z2}!1qt79<5Q3K$dSgjzUo&l`Fje(-Q^9_s~rpZx1 z#bJp+?99dk zHddz>SsAu1Bpvem`&CfW3iH<=z920-lpC)eB9^mPh4fuzzR+`c?qjBcrcWtV7qpj_ zPSwcbe~QsF?b5jtfz|_EGfw!|`Ek|-%MMSgF$$S14Iyi%sXOm*yyw!3stM%+)K6`n zMZdAts`16X0Ek55Gi8Es@hfB&l&01X+&8S0X`_T_d%SWC%Dc%nz;=4aOh^+zf8NdGunlsUiksortMirrSi>ljNjU71j zcC2>Q93&s1<<tjDiu4e}p~ zzb2n4*Ryu5XB0_g7&cnc61cJ)#E~jCvW;wz@>tORhcXeA&QqXuOF}*W(K!a6l5e(> zuNM1pWo1A;8qZuxs9p^VAwL?qh$Nkami<|+>S!=XfW1*Yk^51rdjQ6xVXnt_`dP^& z$9Z0#b`P-q7cxM%|FiEF`>WiIlTeuGiU;@*_E04_40 z3DM);fr+3Q}HK0ukp~k%z9EPVg1j12oF$lBM&({r4xMc@c(Ibf%&A62=8QUOq zvF7oOKi|1COhxqL{>h51umMfRv!BvhBCckkMSTrLBN<@233I;INDeI~?v5dF0#1uA zyRwg7&p-%WY9sY4K**L*w~zoGg}uC>(V&FKz>EB;dR;cx6>Pwz zSY=j|5OptBf1A>GxBKs>O+6wSL0y-#6Z%3RPSqjhRICt2eD}R{oyEQ|qd0Y{pt(4X zMJ0JoZKI@BD+W2V|LL+9eNtYkV>oPBjt{A{+X#EvVY4y^w|@E5Vjy&g z7ccusEZ4m+;GhT`AU^`(W^`n-vHGO*`yKXmi>ppZ;SIZx3dBG4HyOp#H@TksRQVFy zWF7Ot^s+Gg{l1^&&9scz;d6ijD_vQ_Gi?o0cnouP#$J2ay72)iFO7WQskg=DJqrWh z*%;eC_?U(E3S{_^tO0LOI>L@GdOi}ODSFp(4&zY3pMqDqhtm*}YI0#5cLU&q1;VNB z`T^H}3u&Sn?RkKJ%)G1txtpEdBpm7xfWOQ*b{DP+eS80LL4G9ISy<~Prqw=MM{6*T z{<}6;uIbx4mT~-OS2?yBM(y~g4o{&(55}$(AsJ$uOdIG8U9bRGPh2(NPE2a%e?+pUDQ*f$h(Tl3F!U>rbw`zl(fIpX^@z ze_`0eH&s|kV>f=Yt>pG5M7U_~{fBZ`1E~I`?(n&wiAp=sU%LZ-uX@>4#kbX=v0&palW#JHgS2cYvOe7?50vUI;8UnQ!}n|>D6gz z+bB#g5rQ;TqtKNaLeX@Cl%dCs0TI|)dt=o{5krCl8idjx{zJ4kaw@SKeo~= zB@vH_!h3<>c*A7>@!7UBCYO!=jMPQD2|G;e@BYbw2*bH=0`Zu=9oR1_Y4TCKz%p6F)F9cm?NLa}Z=`L}sBb&ULjYQ;bUHH;4 zo~Q@7-Q3I<;v@#(15BSv0+cS|<-b&t0C=!46a+q9WrP3Kd0@`^ej18A)1W-5y$6e& zE-I#a$(eC<^Sj_rYIhX0ynYIhTr~(Y={-sowH=C={p#Do$wgcI*1q^^E`S}s)~r}O zQ4_}J_N!Dm1EKk;PmzB4{7c^kARVtaurx)`=hrCcAah=%4 z*sv5P{s(dcqjlNSsAZnI4O=$-9N_^QTb@@U`qC4X24%{eUMv0cKwXk$EKpfA;REt| zqLqC*xR89hidq%^@MVi@AnKX79c#02o zNb8JBZEQMph*bMZM$B+yZ7d&LG57{YW*s6S%Y!fs$6bt^;zXLgev9c}2aKL41BL~u zVUDuN;_sHe9eYkCQnCNdJj`j>qIbcdM&Q4nh3+rv(1V1c|K5|8Ga#(A+W*X#^eguX z{emMi^|sK0P>2()z?&Jtd$2#wH&B(_797itiWIM?zm66sIPZBEJRfUjMMsG6bZ~1ZpF2lAtg27D+Ab&bCIhqm;f z$?o7Iwdcy*g6t9TC=DbIeXnHMG#{JUZPj$TJb$A>o5f9@?)tDfoMa`R`)fCofrO_C zQ}eevZ6+aB$F4z7_$w)bN_)~rI&MITIRVM0z~Rq$@gba0%~IiXMJvg)a;%A_rS>QY zH$IA?y&`N&p?Wp;0-(2OxWsy4lsMX@$D+nUBTvT`ntpsf74O<}GdUfrlx$-2Eqi*7 z7;}X973EtMaEe~`g|L+}iO8KZh_`n#>Z9JvJcfK* z#b$SW-Z-7-er)2m%B2|S#wa!~WdpNYTea!MXi_k#r^d|OzUryPnn0`zPt{Ok?OAz- zBZ?K;qC%MZ-d>cS7RrMv$+F_WaP2fV71=GH+(R1KyrpDwl|dSm&lH{q{GM};pN)1J z7fm7Xm?;L1X8nSI_Ryd2DEJ#(^*&r)P5&M_Wq<;r2jx$@H(IrWGz+LYulIZ$bpuWd~^s8Z5de`le8>>Q_wTnafaDT3xPaBM9x z*=tQGS{>Imp(W4Ppi-lh2sepFI?$GD-1BTxwci49>NXbCav=qL>Jp6o%xGfHO1Csczu8SSB?j!flwX^sFg9jB@sh)M6n>Tlfwn7*O- zl=G&i)dFc9n_%xWz&CHm4%TIcar4op(9+biT8}{WEi&}@3+7jziI@v3v%XDO%m3jiU+!@#M=U4K4 znvCZS6)23+w_2NlA&*h?pUSUT_e4u*b^Nr9b}Y&Hg{n%@y~=Vh`F|*h>T~J&LD9hq z>%K^<24&&M_WFER-ii0snfZh4!TBjwoA|c&-=O}hh}7&R+LtcG#(FkNovG@^rE_5o zm5v1X7`Fb{f8w?s7Nr}E{*Cja?l;{EyHe%h!L32jmow&@mqq{F>_8Ho>Ih%Y+M+3$ z@x5MRiU20N;8f?<=l-LhlUvjp|C*JxGQ{<|n}624nEHzN#&wNm+m2sErQdp$yYrz* z)nWFj^wTkh}up%$b9~eA#G$RL$O?-=EmK^LLA9X#ItVo|aoimCg0~xIc&m28) z{)e&vdv^C@TH*BYQ`iqIf97zx%k%0Q_T@U>@T=+3LyB*gOda^h)S|}3ceXFOybZ^< zO_-KnELm#It;3)FS8E)PH3z`pTRa&&XZYpzEkGnl+(dp)NMc&7e*DxzEGWIP!YwWV zJj9fA@hDpu)KgHq%LW!6_KczL=QJw*S~1~{)@bIbd4a7UXPyNq&HJ3KnP@Q@%juqJ zZx4WR??p8Wj7V6YCd`u4Al=P~!qB3Bs62F23*(RCu z<9DIG*m?JnQW~I8k~}6+7$8}$l!=$cA;3xW`U11DhTT<;FM?t_PMo}j2;9iPs@9?=-3;5sHn1W>cqU-Kg7Joze&GpyNOr}_ItVVZ;3de zYUv(HIPJ2Y3zRHcsRmtDJ_COT;?K0!@?LYgxVKH`QAG@VbCTznAHdf_ke+mPn+Be8 z5BFm-wZ+I4E$4kI)pioBo@7R^fpCdg5BH>_@_Uk~aqk+O;d3>5>%@7b&am0-o~)*zsN&ni4pCeZ#ARUrCH z_lxU>9Q=E)Q_aeSZ*I(k>48s=O0Nou$u0>yl0jB@6H@qgFhI4nUhMIOoVxrk zny&h->Hh0ufdaQkgAz)oNQ1-_3F+>bbc1vZHYG%IGCD_hNH-`5qmgbVEsTaS7;HX! ze)#?ayRLoSb(N+}lOsxZTjXAu0N|uc8 zjm(v4*y4C}%SEq)TQfv-&2&*c@GEnIKWgq0&JN`IyGIjQ)}y-~EV&O~>Q0mg6a=@t zTmf?z;qVvQDu3#y=eqd2%cS5wFM5-VXS;+M66|(?#utrNQoA3!WF~qnaom!1DMz6M z6ca{ z=o9tOnd(#Y*wU-l|A>TV67h)zt?Gt{<#qeC9T*W~*M;`ojJbg0n|bX%y=jN)3d}s_rh*Fba6om#N5ml!xHU6_F6Z>gmI;}r$K7K5mr%$`^oc7i_yYaON98Fji$NSA^o_vuN zUwzqEinuN(q=?HS2zF&whd0vua7WzY@o`XSFmcKanr@iPoIkM@-DW$c8Be4U(S@u;5uQ=P0!+Dp`{%nQx zEJGSfJSKr!Qq#JF%W4clzKzB zQv1Bp?mVgj{PU4_NcT4kGH6Pq=Ut}85Ql7-!Rrdg_cW!yh` zYo@2QlVV|-;lO2@eq6sj|6_POSR7$vcXjZKFG)f!5o^E(A=eeaTL-5~Me?La7iXi1*n3D{Q<;i*DK|2=TS`Gg)h;C*y zjAa*Atmqv6GU@+Tqt?Z=F<CPhtG-7r@obDmgxU^iB6k~NQC*8wc&rj-Ml!kZUf+`HmyI@_) z&f_;RM{Z6*Ofl_S_&@#3I#QwnrLSL+owWTY%fA2Ti!3o4TWA}CL6dqWfw;lNkIOa# z_(&<{o$+?w`iRy@zSL+ymJ!$TweCM6hKwAYwZWj#vK(?4`Fm$QrucBGe(8 zxWFge$>pC}-amd$lPh2QsAbd6Y|a+*@M%A7!$}M0uA?#VTL-PcqJz%JtlgK&1W&yP z6PU5$@h`5tX=SP~vAbgP&*5I3L42@3A1dY^I!(C8%j+? z-lki~UKP{x-4Vp;Dx?UEVymLZRrme8w@|k4&ni-2i*Cs(w`r?1cpYL+XPWJ=y>r3| z(^jHSEh9Z4$Mt_kvPd(dy}SH62}g%9@gLC}q?4(s;RNd5f_Z;cW3%?6XHV0C=#N|b zT1`{p>gI_$K5$3Wi6F^7c#}M{;WMf}{_PNSaCMJgg)u90=4W^r3exWdoa$=SMwl2! zIy8t$#$E>i|j&{E!twFcB4p&UD4kYU{WM8A&4Ju=xC9Wc{cJ zO2l-^{{bbJ^`L5nl+;Lbej2HfbvwonSoDsCYjtcREFXvLF7%3W9NOT_({N?+O6b>SaKvKFkOEqsKvYXBy;&w; z1XppPLQC>_+0nnfTXQ;*M(arn_>{^a=Y@Yn>Uc&R5FIEw33ohzpfX>jo2?3Pv*Axh zZOAXd!jy9m9bCW?i2NIi_O6I6Wa?m-D)QCkjPTVDnxuH7nQqv)q&gwUWB|`WS z4~>6Bk02DZgg#9J)YxXOTwFW;i2tKar`tawN^pbNe0O^I=?^nxhwmCe zXgbmev9-*N#`^$iexW9(j3Pvm)2m7DN(Bf1Q)xG5i z-%cTV>6cgj9zH{c56z;ufDs})1}tLs4h7()H>HQ(rw5}B%-sV^3dp2LQQK!54)pAY zJ+WQu%5vk>=`QyRTYV$^=$0Z0H2bx=Z3XnA+90pAW#Cq_fz=*%wtOylKWxuQ%RaKe8U?EI z?@tJ_?~`CYGl%6W&O@Z)6nlCEO51NJ=6bYnwoIFs`zl?@t@ca6ZVSy8ll0v?&+v+; zjg_g;H767$DJ)Pk$vD13CDg^6ck;fy)fDs2!ShMe#==52YN3=D zZnRm0zFgG(&^Zw3%N*Y?axaHW4-AP~0G(c840nr!J-m1?{XW1;q+X$a;cK0)obm_W zS16alP~(rur0)t*IJ_6Y0$8p6+wVLZnBlj3pn^A z*IL`*S`DP^j5ZCrZ}l005vfvh319L93T7C0K#1{?SOBVY%@(H;)HTs!JkU4$y*$AJ zEZF*bsqSo}s(s>Ar2~U32Dl#K3KC#yoIH_eKpDMrow|2E#A}AHO&nDV#BW*R)lOpn z5#?wv3F?kiK$}>SX$Xv+es;*hf1 zFDb1`S^MFYIims&jyJZ#rCofW_8 za-2n;bWn-Yo>~^P3i(edjmXS;v#awo?u*#pKqeZ7Hosf^4 zd@x6fpkiBJlGE;;qAtdb>XR7d8DjvykzcS1`If#ATX8|0CChflHzqqUPrKDr-ms)$a6SRUzp4K{ z#LTKnXN-9Ih{ zZ^;at280752fu9lg()S_a4v+7nUvwFpPLuCax23x_wSUEORp7#F2L6gTxkc`Dv_PcGLk`@-D@H zz2ACzQKz-zATI)(2kcX6v>lCiHH6vaim&P>xqInXY6O|j6A>KS&Dp#LG<;`WwUD>Q zi8tDWA}Na1;_M$$IP$Co13^z8!Ml2#2!qGm{DULlijWJIK0oxwB?b2~IEuoxR%VGm z_{MY1ySCVi|LySB$54IvAw8|q%E-Ln;`)o+s9T>~-UEozKcdNQ%)&Zw*e)%W_n@tu zJR~#S*Q>nB>aIwTkY(Bx;STV@)(;(q?rK*a=8d!e(TN=7yk^L(IquYmpEUweZ8m^i z1_CAeyg?jE#PEAn(pDQy?_uZ8u)({+lj_&)Yf=g{vVnhQzA8IEEF~G-IFw14;!FCI z%aeejG?jAXU}*eB>f%ne``Lp!OX=CwIhR>d);CP?qUkW5N2JF*uFuG5a$AxH5Ad}W ztudW@HuFs+lKB2kx~PWHNVT7vUp~xXW_@T1;%B$~ixW;`6CS}q<+vRONQM+b2)=ndL>SYUu zpj9pVmy;vT-ZA&LF(!5P!ePQcl$#F+Z$-uRO6*L2ger=6yi=^tH*#UIjeib#aj3tT zC^q%?=WaV+<*V`$w$3aHCtGs3lpT zpA7CN#!i}L6R7&fIL$yc7(%@WEK+w{l!Mn_YpE+Hf`c?1j_WM#qKc24n*mZ~)Q8zgvWP-GsbpNLIoaRJK~S^6-)4 zEk}7~yvzs+M*=_wQ{iZ71OImOCGR^_DXOGR6;o!YpN`d|DfI4FP%Hx^7mVZc7|GO3IvMmP@WGPXi+w-~KYsr&QuL=M0p4!#f7rTnqt>?2fvBXC~t5D7LL<$2E8SADlJ=uAJIbu!6(^K6@2=h99tJMywx>anx^3%hqF1!SpuVS)aO zRdTVOTBI)ipFhovRtV7i_HHAp2+v$3GRQDH;~q?*Om*oDo{ISLFc@RKo}$b+lmhFj z>rTuSbQjl^YN-gTlA;-q`Ki33WQWw%MF54tC>CnhCjTds&qD;)-`ko2T zzsnat$Iui`xYq5n9{nEQ!2iHP{8Qj%ESw^i(Ctr3C#Kk~2wB{u*>9bg!-T$}{;qhM zi3IIg50v@If5r;k$lzSjRh*y+LIBt$qjadLtH4-wU98ev;G$%L1q~>(jF2MrkLW2L zen6*t2-pi6sY+V!Ycv;n_IUZeqPBDkLcRwO*t0K#1Az#`hoQ7(50WL;dX|*Jk!kZ~ z{1CKTZm2(&hf21`@Fo_bdw~aXX-TL+3s0%}B(s`LyI2bbzX z_p{74g)?VOXH60om56zD3}B$Aa{D8$6Y9wN*HfaYxp;|YX{9lMy0`(gMG#_ULeD}6 zd)Q&fN>*RhYl-6hPHnr;(m$v1yhaU=PNl4VWaqz79xj^H71xQOHB{k;UXfg>Fy!Hw z{{B3k!94QHVsT{y1GiBW1pPJSmnbpd0`|MP)n>Ejun!7#nd|Z7Z8^ZD;QawZh(_te zTG3-n=?!Wc26emugof7KSnh{Bn~Hq|!{_gTC)b0M9XJu0(duyN50YQ}?6o{E{chR6 zu|qF0$bxMmo(si8+DrgHZitMUgmGKwaq}U)=_gqu<=`h?x0Ft#XKh%Rl7=`I_ zTzycftkXc8mPI?Zwiv6uKTVkC>@k6v-4SU*M}UPIV-e#&JXu~cZAg!MoX{bg)Zpn z8N3vm)|{))s!yFsNP$2L`Qzt=>_)4l&kp*q!8mQ)SDZ2EDbNf26E}7_zXErm%kEN7 z+zUzDz`|P?bv;=%Fu*39na(aDN_JK$-Rrv3N@kL0E;(BkjvDSUz_Fu+CcA#!03Ziv zppf+KDTA%bMjz|c;eOLwU46CQ7Ngwo1Bm`5p3s8S1{~nFK;iC2VwRV?YIgKw0&m0D z*`61ltlu1^ZoaFrPW|K8=ro!dvw#-UDNwKDfEAQWF9^!MyFKjtbyJn$ErlS}IIEeS zil90|b;g}oGTCc`z7|?U^bWF2A{hSs$~dfVuNx{;@MOmLZH_nZ;P{p`dnEb7eo)Ri zB?WVPGmVJYiqi0>=&o+)HdE8MoVKlO=qPpkVwC4p;}l~~&JfC0`+EW_8jy;1coZk# zlGsGnyt_THq3^TZWqQydP|V9Y4=am(wc#{;+;uEk~*Cy-fNp*L}@XoC=rFi?O*HyZv$7XfNbvl+faiG zXGtgT(VpSjpJ2@Gy;~V9@RXZymOtRsmpju#ySaMXy0NVVb*Wr#k9nU?l2T3AqX}Un z1OO1zQ+Put(Ekx>b8KPMV!XdPC`SAv@}WB=Yz`<(=zL8R|34xf#5IeeBG4OGWYu%A z3ZBJUtU{oXqPs*re5HTxn)(Bzm**&M1n|@nbF#NR0j@P(pW&n$SL-KYbWZ zBJrA{MU9I-Em>a-L_`q5J@?5DPIpDAfEds zI0FzOYoCerdyfl#9+!T($`bt(P2n{%BfkqeQ>!|M@&vbtbi?;Tf{o)0jKnSyta5p> zVKJllpIG3{a~%8ffGoTcTntA8Kf2fRhLbO+ed0ZZ4+{4T%lhqtcX4^(!jRYnkgWWa zh--3g#bytp8Nex8{iT?{Hq|BXby%(U zF6!qcDW|W*b2*TJF)12SGaZY|fe?R4V9h7n5lnjsk5@jt;rjO2@G`9_Xk|$|X>c0^`W>pCG3>kPwR*hIk}}Y+ zG@V6mECRQa0%hLDSH<^mNvOw+2ocr|`zR)8(NTF^%bsTeE!p^Qt+OTa5bb*l%@F zWfYozkJ5#{@@SY-UdbJ6vi#+VRtDK|>~c3}`;kJ@hMpd{f>|tFqI2JQEi5WtW=Bnj zv~eYKO>4_AI2_YO}B?1SZO)jH5^^ERdBdj4j^d@zsKhK^LV zFP45yJ4N4`_lxEUq-pjkOXukGxkk?!Iz5w*s#8j5XI zwruiIGKZ2GZ;7w<90j4>ZXpE8U0(we+Xc_BzoQ`r>07#0?j;4j0uQLGxmntKfCe=b zUl>F+O2iEfZJP@nnx9&kiOh_P&~Ptf5`ek2a)T>@GbVcgMQp15~h-p0VsVnDQ zp_78vUCMKM-`~yqT5lnj{RIKX z2F_v+3z}>Nwm{8Rh>i4M^|PptA{^t=Z?(FI1?|(CF5Hx8U1$vCgD-e`+FCfm?*3Z9 zukPP`d0p-jTS)J}*+k?iMqbr3eK(^-#Fmee^Cnt}Zz3>Lh}z{m8>k-ci@OXLv5Vfz zSzN*VPU4bgAy7_#^*ePK+3O7Y`={8{Ki4n!D<fKXny!_B?n`a(_}bBjm>g zHhwdb;mo?i!iiG2TWe1zg6%iY#T8uDl_y*3OtQmcmZ^gyZ%dTz@n<8yTCTzX`MM9S zk@gGUdn4m-Sf~==xp!}cMak!$6F;-=Y2cCl##u#ca^@m9DUmv7sUF!{lm0{u=v}$l z7Fx&SSSlTzGGl z$($zyqhgzpjozxnSS?nr$w)~7_DATThuadf`M2gHu-7?UbwgjDlyrjnj!0_?XrJfG z=^7;ayii>%zZ;(1N9piJmE1hV!!eZJOY3^91EM|O>R3BRHn ziubenyji&9Lb|!Pqv-GxrjcCOR^6JAoi999>n;tWisbrj%SS6cd0ELdVffmT;Zxks zl$E(mt~_%;N@QY=<|RvP{WgMQLKW~aL6))2s*~ykrP#(+=}nNIO-a9nV~9wZLBu0_ zrJ4xL>nSm+sIbFg9ye*|d)F2I&KppN%xm4|tYXco{}?R<0$vgrE_Y~mYB>b zK$6a@3=KUGp20G=hDSC$x}?kwPjpjGIy}~s+>Tyk2Ney}D?H^+uZCsj>c2p6 z*l@DbaIyW9L*mC1bW*|z-aP4pe=@T&ydF01lq?k3+sLhV^zR=mwLPx-Y*z7xR>_B% zF@^so@NV%CYo?TubDm8_b;+<{x-Bn;Sgx)`OZslu11I}!D-kvU8yTr3WwV}A#hpHS zvvLBG;`P3k;B6mCO|m_F^s6C2@~xGtKtX@$aQI~ZCB zYFVJ!QVFN<2LTgNvIaXsqM!FeGO>`#ld_sMxRwg>R0CorExzv%GJyQCwtJ;sdW5U- z#J#`*+OJ0owBB6jQ9i!Mr)%Nt9bzA=O>!%Ks|rQ`Jo>nUU_q{ez`o~y^n6lZHFj51^!nXD`+7FHK#OYY+s}xSyhpHvIPm~2IJaJ9Pr96atY$7{!QsFl zvI}#<;y8*emrkwhBW|&E;gssU#$q zG3oNoMG+qVytH4f@V^w1Wi1KRZv+XO=%6_H=#YM$D2YyUi?E;uY|9gzrtKrF^e*hP zGq6|uuHHg>s=1mZP3jKYjmCqHm2XnR21+J6VUJ$Uo0COZ_^WS!Q2{ca#9>1n{D)$f zsgue(-O*Hux=2m^k``e2GdY!E0~_~-FR!fab{s1%##QnWRsjdDvMNCw<;3ETnyw}{ zy{|29dM#L9gLg1ni~~$UOPa^S+ukx^4^L;E2gPOhc#t+xp)@?NMGfkid}rNL==_Ko zGN5!6p=-RfZJ+;k4P|Q#4c&HI@CTgnXqHGXgIFU1_*50A7!n#GE`afXHHtZv>lbL? z*P`_)9uRADr2B3}Jxlk!#S%o`UT62{QW_K2mBPx%CQBtp0+y8R5&gz$s~V(hat8@f~uFfZLysJ|0p9?Ea0JlXc#y(ePi(8j zL{CZCGOi=0Qo1)C+o3z>X28HU$+)2Ri2tQP#gB%Ripnn^ofi(}vVg@^S0%17B@3%L z8!NhLFHJ-TV~LfN+JuV@ly~dL7;_U^G_%|dK7CvXy1}DAwG456v9jv9v_uk5#&aA5cty8M*?}PN0J{2QMMtVzD zT2&=^1-q>}%tvqEDL^V4dJ6~4wSMmh{_JC9e-XDZ4CjzQF}2xF>QiJ(OJwoPCW$JT z!8T#@!d3&eg^7{^98@#2RGdKqBD*ehj_IPmUovK>+7eGDG28U_-@0$EIey|FCPw(l zT3?|_h3&*mX&paq+)co>mIf-Sc9sK}^aI`-6y|4TBuc3%i{=GQw@z>(4fsHe_h`#| zbH*!HYa^l02XSlfo6X4-M~e&3T{RB=j;5E>RgYDOYH1YjNGIxMQcWf1Uk!`8 zC6KhZY+fi7)4!P~pCm(Uisdl$1d<|OU46FCgdT?1^SPO>dNG9U3pjGoj0BOq0x*|k za~95Shn7{HhQ(QY_>wAJF%aS~Q`NxK+7_P-vy6Cm7w$zQ&9hHgWOZyf#`Ba`?$$0Y zBYW(^!S@40^F2j=lApF28(|`Ebd#;)6{rQ4{YfGMO*B7m3#LAa5eF|_t%H*1TO;pn z5GN$@@wXaA_Udvm1$;ktJAS83r$~sV&SLbw%->h6e9szHZC;}Gi(0t8_6N29oY7MO z>1cIr%`naEBKus1KT(3LG_0&+zz+ir8!u@t*n$}PilRQr#H;YP{Uf3?Kw7hE@bjvW z*L`06d#t!wLm$99m|ZAIU2hG3l#Bf6>@m2heo5PD1TI>8c%`b z+^U{{vs9&Ma?xB;v6#TuyXoysIhGd(hW7XwWx0N_EPV9FZqooaqsAgUFK7p z<_^V~oYUn)zLCBR#jLrfmd-ozHb$*IJpd@eN9YD?<)BTYt3D{CHk(!LUZ%37$thQJ zi1>O+5&yAlQ|D{kwFt$>_8hAsd10zq5WaEOFD{2P3)a8(@|F?kR6)U2?R&lQi>af#em7Wndt(C^xR?7`YBjU-Pjn9@QE7t{9Hy=2#?62Sqg=4Gqltmh& zj$9pck& z#PI>rz6zD)xsQ%3@iEz_u(!PqE4`#WVcE!3P-w{T;chc7m<*rj{IXWNbWjJ&4{)A?qWM ztYd|phvdpz&pkvUac{v?*Er`lm%LvwZbux`dO)!SKG=T_AZkz3-_jgjbJVrI#nJ`V zpDZcwswHT^F{;Xh+y#^;CnKa-HW~A5L{K5BkYJs3kVx4j;8!7@OW>Uw2o7tRAQ;Y7 z5$b3`7v$Eh)pV?(mq`mNXTekIw=qKQRi7$V(}EM7c8nf=)(KX-RZ9i^|4ucOP1)+(@M+)}4q)bYOoI{AE9z zxfFlR%lqghdz(m+nma_b{^`HfjlWm9nR&Lt>HZz5f^y6 z{m>?SCR#xQ-g~Z}cmN#hPb2ZoRS!mO6A-JO*0$UftniKT;gO)L7)E=a`#0c}OHW~W z8b`7K^a|2nH*}UY0F+OE{xHqtBXQAX%4FJvImW9%Jr9wm(EYe{WIax@(NvQ~V}Vnb zn~yEYkO}ip6GICn45wVBcL47$TchJ^ec2rNuWTnePR>1@aTbZzv~K-f{vQYqs4T;O zLOXFp;22!t-Uw$}2|f*sIFr_XVq^g~K|@=t*3dn`T}|_C<3v<$@jm^}Yu485p=&I* zLE!kM=|3XEW3-Oml(IFcLtF~~Bl?Lt)Go$4)>5MI;eYXa7eZ#2h}JI2SQHBq!&-!e z<_8}aofirAqJb)CFf$G?D<_#jeQG6mWf7|u17_q?xxu~9(c0XRGx(+90+#cg!`%Vr z;%5l+Ho(~|#`aP1v)e4I)iWTBtW!XcMm;s@C$!HFzoLaS7w0S!OfV;i1j(Puc2%W*^mex%75DHRoMw@)>A*2mb zG2w*;^{$=K+?Zc3wWz%FbPooUopz}ptr`Zn2Ug^@+ge$mKeFOP2y}+Ue?-P-*C<>g z;a6{<(**SRm3<=!h*gH*gNNf@K*|TQfSbv|82IHo?9={mB;<($!W2W`D6yW=x{U++ zrS1aesnLrRs6IImq#P}${Xb^C-z81~Hvqr-@sEhdieTn}2E0d)1NTs9!^`FQ&>oCR z~{oU2wyMIbjld{3B|_{z*b(@NeZ$z}@)Wk(ISmjeeT zW0;`06_@Tr1PBR|=6babExi_6#dG5bM%>pSZLH3R&sB_t(v|=%*okY%Dd1Ln7+j}YV@cuxXbfc3Vw3)!9GW)cG7yy;oT1Fi`diwUwuf1Y_i zR9pZ5H`)Idw@raS8-o|t08tbK4m07lf~c1Y9cnMt07;J0R&zB!G+p3T3P1rl{8);{ zHo5X*_`F>IOA6v|gN4Bm{EMK&8zTe|&xO+isN;l0__}vjRMc59jn{yi#1XtZ>&QGW zR;UgRIj-w{+80IW>>~mZxQ1oCB_0W!0!|Id=k!hx#Du>fsJlcY_(6&ql8Yha-rBGu zz6^4rQsYCoAkO9qRO=kb$poRRGVgLkqo5YM37)SiBy17HKF@DO9mjs)Q zRSZrG2fF~yK=+o99Tz-5szhsR-!xrLtg#HLpW6trYM`~vFRXyieuw%!39l8J?()TY z$|U@8m%)Zq$m@hUU$)IRVdRl3tl8wXx1wu+V--&|3`3zj`IYg2ULoO*Izi8X@HxPC zmg=9ErTab?^GCI-*Z|1|dCUbxNHC=2tYUBEVhz~JJ#q=}l9GEH>s8#hdm}WtIQc4U zt=jRd@b+4g&?3VBO6?*cj|mHAU5&dWUmFXpIt|sJGn^^$*$&HTdPg#@f$_YQJhswy z$L{W#(tElTz7j=Z3wi8P9=}kyHy}ASbd0K>ZRaUdj5G?#)Ef~oFstOvY+M+R8ET<~ zJPQ(A^!364w{K19=)chn%~EssqLkhx&!WUzlT2OXLZGhwjB(jP;AmZYk>-;BK6T!I z&V?Qr7Mjs7PWb)(uF~~vvJq%fwP1|TM2>5W?-SAQhW`?ZEjnmlP%3TfGYWrTOx_rJ za&M_Xu|6cmaUWz{j;@_b?|17U$B-tplP6~T@*YsB{fjAWQN};b5ziAi1~U? zf-xo@=)8G}9n>-Imac|~{}QKH<}|l3sr|_ZdX_hCVVYYv)P4iaESn@`rpRgXF*!q6MSBiycBm-( z<#`&HTaaM%`$%^+VUYJF;Zyv&@pCcLaGd|{PJhJt6Kj}8O?qY6^Cyd9aCV~>@j7zKFp zvgi)`^yEmmVIM!u(dD|@{HZYc{vDf0jXAw&g(uEI;<>BMj!D1AolJAjw9*^u`|eQl z_uu1hbDlQF_o(%*eCRtoe|h|_1))D)ahnv=PADbflP*$RMgJtz@jlUdKby~(5kj(a z(<2gJP>kpkr7E|1+-M;>`d>Ctl4iThp@GRgd2PL(3G=&Sb;0+4#I6)*ILJwH$?u$` z{N+0q+x|L#Qo+#vR-|YH5fXTsQU!-)F0rc(h%+buK6~qWl6Bba5;D_uS2~8%7J&kf zH>+41s>Y+qKM@_6Kdvl!Vi={qycM6Bda*UtP?NsxOb}uaut!*HnmBihf;UrAR5D`| zWi5kaaOpE*JDZ!<+^MqUT=W?&SelOi>0pn=uKb(p2ey6y&J>+Q2_4qwUk2>FvpKST z64xX~b9LRN%AAt!hdZwhm>Sc;8l{3qOna;ICCj7SXQ`RDsGp3gZm#@t-^qik9#}~W z)QN5hTC^(X+v^&Jh{cr-E3oP@&g!I`89D^LlFz2|v2O0+Hwe-onCDs5xl^Y>M;$zv zs?~;|O!i}nmG{gQe|(tW_4A7Tpucy$TB$BITrqRn@Bdua&Mopsp4Nw$moVm^rl0v} z(<@PUjQoy+EuC!aQ^nGb$Cna_YAFBHp-9oVN{jM}`u7a{6XxpIoSRH+gff=ll zURR=`-vg9J+!vGYyClKj5L-ug`_kQ3*u#MoYx$<9NPR;et@3h8LsM$Y`!D+aME#nd zaN@VV`lz|0m&g`N;-OEasR~dZY0hUOF&o1U!FqnaW$qoBEc;w{b-af&2fUR<-NMO} zU14exbap4rUuN}W{NDlAPOrf#2IB#OZ;H&2sIt| zN~SV>{qc*AT8w6dmSb?a0{MITS~DkyFS`DB;Cmae=PyRAAdO~oVPd9Fo=<--Un_`b z_LEc|CWc4xa~9_nFeFX9!nnT+5GS1d`sm0 z-BP9j`veqOSjOfZ&wA^}<+Xx^GKR6mcuWo7To@R z2EK_jBqc7%S9WBU)feS%u(KECak`fB3}N%Mca|ySoq$T5t}73F^*2epPPmCpvX*sV zlvVL4RMyzpw}Xw+Zk}aKzNvicBQNck|9oDsQZKk$GPM!4Q;cvjoJ_JXCy0V#xz?3S>R2I3zeOc?6sRkDGMsPD#Cslp z;kEca;j)YT3>Ngd|9$o4(>w3r8i+~Csj1^ zR3n-#dL*J%|29LkEL){L?`X8F3+z^&sAE(fET-ZrYQ-%0`S+qxQ2ndLUZVoIn9g=i zf-iqZ__cyAB8*M$=j2z>XTxesA%>qe)ssnEg_&r|J^@Bw!Y5;@Bz>&An6fO&Io`zo zI*D52;t$dJN>a;URjwkoU*FKMx;HFbsmrrUWn|L`L)57*Ec2&$lEp#gUyn-(Lt!!r zb6z3NV^tjI#hVVZ`#cBfqEgW<^DYMAgABDo^(H&M$q4r<^Rt{W(W-?~`k0#fAeq&q zL?uekJM@PYX$rC$Bi1v_6K9p8Tk~jot?!RpFkC33?+^(wVYwc6Q@7qBDAm}zv=?>9 z>NPZ}zEiFao_60yxIFas$vvQfjj48~f9@sg+Hw_3T?9Nb)8@Fg0UDvn1qw?%mQBNQ&cY8B0H~{TD~d8(Pg< z@$*4i$8-0EP)9^Kmm8P8ipd_!u8+umU!5{(p|JTt zokGI67{mBYPiYiWYehyz70;OykC!-{t!Ke?TE+w!~308x!cQQ>L#OmYFu zabnF4!)9>2!J~`vE`E93{z`Oo>Cw*Q{bH^iXEmoc9ODu6!<8@Z7}^m#%s1_`Ida$; z!;Uo0TU-p5I^9>!KdFX`fHIEn^+!szt2E5GCW%isZTKzL=@>kX)RCr6 z{ykzTsdF2kb98*~U0J?v*~AEcjaL%+L%N8zx9J||lq5DH8y}Lu!8J*Xn-7qHCJv^= zBoksM1!+c^%y~PLoZnZLGTM9#8I42Kex=WJX?%@35z4!&A}wFtV+PB7-L~gt{$il~ z`?NEBT8j=aYYi`L!JWyazNi>+iB-)i;aHYe<4SGwK9upu5xwHK>oYcbT>qJTrzR_} z*zh-=vb43*jf-~`=QB_A1AM!m-uDH_=W@@3=!vk9hYvlO)$0d;Rq66)Q~x$us8T&0 zsiz8ybCfXbGPJvFlD1G*+(pid(&7D&7`eGi+ zZ!+~|%a)AcJ~!DNL-S)KEY9oBM~t`;qrPEXWB7s1&hw5SZ}P>uEz4JVZIc#k4h3Zq zwMzoOPgEaYZE!2R5=4BMshXuu06N%~H^Hgb`7FP8t)tFtdWj*zl8e^6y%_23|(?^zLtHR(68S0`?uGJ+N|mi&r_d}s@cub zOv{EcwK%@$eejUJKIW0XKc&i#x4R;C!inS=sl1|02`^pT?g^84>2uViC|Mc&w-uI~ zV$-Xas^C(d zK%|qIcoSq9vCJZ^=SxYaqLoNtJ(tbysX!M+<}w{ucX-~uMf$+<10uPI&1kiS!Nv&x2(YeXVn@1}^V^gVO^|*y&(j%|dfI$3|AN8h z)%|<$YgKbkLH^~NaBeO!!B~#drDI};FsXM&ukJ^X1oM!NcUN-Bdbwr@j4o1|acy)` zs!>D@rkJFWhBFPHzEJhDRb?LzjqSACH>%6uGIp79j8Tau{e@TYo493$uV^`mCOsY0 zln+j8)Ve0q*3~fuo{LLEKEA6LWm)rU`qlm{_F;5_>XxiQH&3_~4Xl!nb143)?_(i9 zw%DNp{(0e)QWKYK$5%*hP78L(gaQK#gCk>ooHyxKoFvVL=JgA@7>!B>zifpU&-WB% zxs0X4O(Vsi!+{Ay9B3jpZT362s%H-sb3m4A+KF^ecVX`hUI_W?v_5PA(t1hug$;e9 zWYpNQlx1smSUydV)l^Cn>+#_ttE3ZZpN(xbr4#>Fp{Sk1ZD^pPsRYY1?ChRK*5xk8-gP=bsi+!iOg$IAqw#E% z@r#|ef&PjuhNM5&-nsv6a87tj3Z!8E*Jb*}SH!pDx!RZd?m>UPR*i_r8Q~(H{vTIw z;nY^!M*UJRlm|+2FYd*PTk+!VTHLi14JqyfcPTDGin~j2iaQj7yCpy%>B)O$&dm9~ ze;_m2+1Y#D*R|GfX}aiI5eXAxPfVBNT)3Z`vu_?kP#(~_zSJ;Q0Hda;~V4zk9rKt5mZ7V%^ap ze?b9b5!J7`I@VVEiC@J>d8|zo@8P?f$wj>O5pypqPAjpx(cNDo7L#GjY)sahL>JTq zPsPmjCMjE;Ay_JSiH@9l>%NW-nF?TI?m4RvXS{|^&7ua2g|D1CS|V3aMV-8FnlCjg zt`x^siJ-aR6gj+JQI_fgx%%+B_%83tBAVc^nNUp4qctny zaa-QqQ{8ufYuobW@lg^O+gsL`ZtAoqDp$nr$=j|0%u5#TY(Jb}aE_i*IPk5Y0@KNv zXnLc42BYfA!Zd#J3}%1Khi-hXbeD^~^-z`Nu4zlJsBW(Td$^cO(kPQFAL)4#*v>?y zhY@0DGXHEW4rfYS)1-}ClI4|q3jWqh8vPaMAr7uSLzL_e7b;ruJ?kD-96<*-jkQ$s zHG4E297-pkHRvj;0549XzcP7rNfB!)xApnQ=Q+0OhN?EVjm0MdzOZhYA80DIZ{dU3T$$1U`9aH#dfxUShrhZz)hUx#cL{D=*1=e#e)PGatc^fq@ zu#8V*);XdTrzo=0Sdp2L>%LT#3HW`?44PMKsXG3RV|sPN?^}nw!+W`iEm!J~*u9Bm z^r*ATGougn_)kCRy6Sf9cXFs4MzB6ODd0EStXe5&Nu^ZGo#U?Q07}9k;P!X9HZJ;d z^qdis_X=(#l{Qhulxq{Ai0ue3wisxP|I}kYZ&*etfi+b5_<6TlfS{eLoN}X{RQBXS z`0w`q3#XqLn46SLlo#nbLYC^b6CL|md8@W3PTgwJYqHykK9xanK8 zI%dHjKv}HZQEjMHqJ7x!hW0?|Hoh$Lui%k@@5_bIQyTO%Pi2h}Mp=-5Q2d9{P)`>Z zi9`B+upG55PHxr$zv0j@!b_XY8|HC-peAvL82$Z4DW4?sP#AG?=Fl#xBG4rtP$T>4ujD+CnzOz%#T`^Ih2nt^FsX$y z-I-r6Un&`UsIxdX62GG;5gY_8tQpj<`rXy`V>I6*LiG3DSnk;aLt&fgx=GaSD7zw~ zIZUFSQMW4Rv}q&cQ|`9RN)s<0rWV_1YL?$}y&8ytHB!^Ce1X&-o^UCTk=k5E>P7_9 zisvR*{>rks)6)FQWX`Zur^N46Zf})>JA^}`f`O2ykT~Ve@myi_vzGNX*oDEbO-EZq zR7yoNme>~8WWg|$fuXrdtD^t4^HCiubck;#nTo~puMVFr{up;Xvy!3Q z!@Y};#nc?aOupG|)1IAKL~g@Zgi836`%yl2`_s@2>1tNCa5-7|d^kzjlSYV21|zxr zcM6ZxYb(L4jMVqO!YTen$4c8orEpx83B%s+9^)RCrOiFAPqn- zj|z_a7DRH<`KJkw#XFI|agZgkwfWaYC(D+W=Io+&asKBwUaA~~ddwH%VX<&UW%9-i z={L?lH8X@pCX=*>tL6t`KYqDib;V-Gupc)riXb=7;iiy*EDn>*{12GtxA|+m>1}LO;@e>v=vsV5?&;-x$MC#e zmVz9N^V}sW8T%)XRp+CD;Cuo1bW^CzxJ0O!ZgO=)jjBmLQu3KyqXkPHcD1k&wl#mD z$mePU=8@Bx&Pyz7(hr&M9?EH>!)dXVV?8X{r^J^LJ`0h#a;>Uyg`P7EZ8cvTM;AnX zh9b+qz4)BrWLSsCXWH_vK6#fjUSl3@z zoFmOnEj)g$Wi69MJQGRJn#BL{%|4GifZcroQV6vF6PoLino`X9es-4tyue3nO}8%d zC1~i$oA9h!Hp_f4jN1{J|*x?Emry|{5)Jzb%8w@=P~+k?zX&D&8RQdNOB zN(X=JLlkzkjH^9p3>zY{c6p4GsFA!z_k3G2$EJQH_i6`h-(tPi!pHh?F4-bPmsdB{ z^2{m( z!9`T13OxS3I?GW6!EG+a`Rkt*j57F3bg-Lv>{n_SgJR^qMwUeVQmV62p75$L#`(=B zv=VQNIl;tNM^nIL_SrJ@1oU}FyLkHh(>KQQ^p~lmzb(Reu!)M;i^lyTw{q6fPSaQa za0k~)GxNJBua13LmA8fRmSlwFRBb)N+P8={lAt|UxqhWlqu-H|gjVZ#!@LnamTCx(O zAlZ5j_0_nlEMBvb6k^{RHN%>FKWlR9%u2iv1|Xy5W^w**6%}CxZ8)a-PmEkGFNRB=XC01J)s;XBi_Orkh@&s!J*v2iMW;Ux(+ zm2kH%?&}cIhS8>YscS1x!ryT4?nO?9)6vsik+(UN1w$#%7HsIY=}`D3%9+V&%6Mei z!V7q0szPLE6H1$7nP>QSPWv~^nesfoYQ?8=rL+ImGk z#jW&-U}d|CCrGy8lUIwH_Q7jqA;nF;4AI;PnYQdfxP-q7)0Gx4_j?#u(RUBUP;SujR5dI*Y^1%bTK~O9_RSSSJ^!-Y~Vz!cuI5VSXb`nb0w#KYr?Sti@k+4W9@k> zEi8%DEk&H!x86P-S$XSsMXS#QQDctfh7*^}N)+8+wIyQ%c`ZK#3UJ3((&B@r#!-wXaJ>+guJ(~(6qL*=*!W_9aAp}mZG$!0Yg}@KJ%-mutt+m z|2=JrAn{@?{qg;$EPsuEZC+PoZu_|vxRa+ zNo6r8^_>I>c=ArA;H9=sY*or<`!>K&E7p=+y~9iPkLS3C9%^EXeS?dP8fpTHa$|y% zT4Xq{zKbcp>{ood;XfhI_KwaP*v~*^braUm#u3A;(77MFIGN;V&h!WrJE?d2$t$nB z63Nfpt=t<+7w;*jWJAtJLD#;n>2COjw1tvNJ!e+_nTGHBu8L*U7Rhc~-dvOLHNqzG zK_#xTj59zrx(@kUV|D@HL=+6LRIIPj{cb6T1=R+8cZv=Htqsd(OwD#3;TKx}$Pd}{ zz$YohkoShZXMkvws%eT1o(jEIs9QEE>h2&hQA?#2QouZ|1Egu|2X)7#={@D@yohUH z#&b~24!!xjwJquO+EcA%Jk_?0O}o7eSz%pePxgVhiDMkpBQ6r<@j7mvX?@+HGdDL%Mv~>e z&U!r|>dW}&XWF295QjzNBq`6{qOxwt_*v7teeJF5bQgQl`zk-dA^ddQ){04x)nRc5 zI$k5Yb-}yIG0LR$4KEj$mJdwNB9#VpcW5#nx-V)lO_*O!w`CK17lsu9#s6YH45la? zkadp}yotId|F~Y8PR&7kH1)PWe%>k!MvZ6bQ+0nb+?n<;x$oV&Ouw6In_*JG_c=x2 zgnjQ_;h)poAVufq)lWobuhr8$pyE&V=QMO>^$ z{-9xeb2=7#j(w%H&pa4YOSB{<8#`OW$^A{}K(J}0_iFY*Q)Nfib@E}+=RbwCrC@nW zXSsD<8%4zl*qcrXeN8CMPRUP`x6|3=LW-oqLA+dzHDsMqyUv8@9<|ks!YzAGE-M*% zO`KnIP%v5EjC_^nb!Ch2nJw0Ae52Ds2pgCE>x@ipJ_m(GA*=%j^&%p2&Sc+KYBeTC z3L#;Ndg$2-QB^>v8i(>VtB_#J?T&S5#480#WEU+h({`>r%fo@;+|}tDd&h-^Tc~CH z3T}M+JMp9!*Nuu@WQn$O7BP9fZ!*<}$A`}5#^=Tp9%B`}&-NI9mzn3X!cU!venj59 z0+E~Uv|$h6d*k76S|1OoVbU3%tJ!OQoT#?06kKYeS0S_edv&4hws#;C`N4iZ+~A}hCyCrqQy zfcD?Yaw!@wXBudV3vaxTU2$gdc;%_`FKTH6^}jQuey4I86?(1TKbqH$r5cs(%wb%> zpS~65z|D5XDoNUQR1nOl<|FTGPjngdb#E+?^nm9d#_oi#AZrvq8#hzGt8PvfEf4XJ zWXI>tx)1CIb{Dm~nzemrO8xc2cCiqq$S~L|JYD*Vzcz)+m=V11@F>a+ct+-_y1Vk?@`OZ#?kmfuCXBbWXEfmBwF?tX+4Pp9 ztlD@Z6MecCwh0El}f|@%TDkDFC)6&KH2cS0F zHCPn3ZK~FaA1PQ=~U}^wxn~JNL(1UC5&TVqiXr5FlX>z5@W%V z6wgdUD7+frP7RJY2XvGwObRZbSwX`e(WJVpkEK7%SKoYZp`Nll=|LP*b+7~HybxFL zgJsU1%{?R&atx?^+hZMAhQ?8zoVt4HfsSVO{UkPlGBKCP7i-4T>Yb_8N2WWsV{3D* zbAIa6i7f%IBvLeaiP`_>h4-DoP{Q{)&+*Xb4ZckDUj0>cu7EdC2&@*IT2#H2qn zo%_Y7NqI_g4zFwia_l?{qNwzwU(;&GGO}n4{rtt-_wIzNrgRvPK(z4xa8chT@BEyT zOJm#1AYyIKEH){Zsq_U{kbz=MId+{K)lPgt zox}v#ulk_f8%7kTJ~?M2oH`o)EUL4K(Tm*kJuUkB4RcIQaqu#ObN|j6aLZdnk4*%b zP?}cYt`%yE6-d`B^Msigc(Ye2TQ-cJ&1R7R?i-DDTvpVjeHrOK!JKt(S+B|jAlbCM zIfLGY?jm&&m|cRQ^n>NAJ(w6NTDueEfjlFJ79_9E`U+kTRjBXGYGP=FNkOK$LfG)h_j;F>1Wux1Q=BWD2tSFDmMz>S)XZZ= zFZzC8qi)H|y>R`S-zrAE+Vj0@FZ@H+`hfjhBw7}G^w3u>n-tuZaD$TIHH$w+(|$_ZQwrN z8M@mieG9tOh)zJ0nqBiq1JTjz+nM^V#O z1L1LncIfdxI(k~al5r67kQ;7qiffHWehZ8hN1?({=N_oZz+cG9ys&ecqbFB`TTbKK zY!5M|mCc)5D#Q35V|YaETEw&C^_ROp+|RK~0tJ{rrV@-ig~#hXzwc06FisZ$q*-gu zBvG!C30{ueLQKO=e?R8io{MGEebyy#nqF1ApT57Z9jReknhXp&LA9Nk8Q%gu{cX{( z>$OcN{I22>;P?mN|L0*nUkL`1g@Kg!lyZ-{!AGVgeU(N*$M5&#_~MFB6lNu7LYTkR zeHg>;$6}4Xl3(;llqIAgZKS&f09Alj?+{s!>4+}mb3ppz++AK?>am#{z~~=_h?>JA zXq|$J4B;d;J_?ub6mwk@RWm#A^Clf^?XDib?Qyw0n=<8@j(#d_`ZgF^o@0I8v)ENb zZI6gR`w4m2lk4a?6!~fE7QWem1uI*&Ifi1yLlCYVqvu`eP1EN*896R*hP;%h*2`o2 zLn7$bMDb!VZkOMXe|$;&Xms~UqfQ=CStD}O99^Rj+h<@2Mg%%i$ea+BZ%z#qnp!$sT4pGMsgI#^TKDle?UXy5a9GHboE%Jjn=ZChH+>G&rd&)0S zrN?9``x>*qD1>IJzD!jb;Sm0oc~b&n6peJ3i2|4&ci;eNlPfi4P9|#U8wDYRkc98U zA{AS9^BBsw@uGa(a>bc>Q%uGr|1jn?;M8$<$xkl+bDIICqL5lte%IM)Plsue!w1nd zKY$PnPTel}9Q0ZL>P>Uk-o(M8flf8D>9dTB)KA}wZPy=Ms8V-`-JM&+hIa0+7~^#o zVV+T_>5T}iH?S$n7EW5)koc%g8DH@tg>KYETPdFJlk$#M-E#m?M50y&Hq+&eHZYe= z!=7&*AO(>yq^RdZhhNLeMe7sVOZW1`zCY%4t0OSY=XU!ter#dqvTNfMXtuljV9|XY z&3%)S{|iX$FAAJ|H1n9Z97^ywHQpi~4&Eq{GtA?yTszxB`-rgj4FE0e@ajJCO$!-i z*D2p0>j65tJ_WuPTegk9ZpWPfDTOO(xst59u@2peSUl@?je(lChR_#;)Fy1=1iH!( zJla;gHJid4^0AYKrpy=875A5?K39XPmh;(~%6P3jmQu70?V*m6%{I7*s6@Not0mBz z5`F@^8Ct?&nPVn-8HN{sf_CHekK(i*sg#etQ~4YX`K~~OQ{O-3RtAwb9@9`c{~XhG zY0-5bCKpqHGEk3#SDAt`TL^nVUDc^Jw9?Fuq>yK%+iUbHer1|iqXW#ao4-Ye5o z>KaG?{as%fH|enGpKxQ1k*ZQm7#l-C{!})q_m*U}eJqrlxd`Xx#uB8!Pa?^`8Hf%PXmNwOEkdu21jEc#ce;qOM~ z2~EOnoH`5Ki*UPj7uc!vD$_)Z=mh_k#SY@UVRGl0&0SqvN+AIAx@LxIFy!w;Q7p&v z$M~k=Q}O5qQ*-sMw2BdSu*|cXs&+LiW}!>Vu?{sg`U_7^pG0@a9$Jyv9zT0fRP4(N zBgxrq#(%7q<&Rz!!pf*xmDVNNM4C|=U

Hx@f8xYu2ns??|P|*rW3&Q-F<&a{H?tPssNEU>$|MSrS8wUwWJPkt zO73g2&~cL+mv>pBzEQN%uzfW<-*-5-aS$OvyNCnb$xaP!<5lDjR&}$ueTwZGQVvFN z`Yn1XC<13cg`&r;H59z|nmls6AhOgHbWhBj?8xDN`1n@!(>cB*BJy)}+niK1q!lDr zZ#LB8i*%kkv)LThu{NVxm#WzmKKRVP3i8TmF<$);!>vtkK&VstgS>D?wVPqn(#u5I z=WMy-ai&^dt58NCAj`|K!Cu-;UnoHzJ>n!;5x3fw*bUH&NgJ zLPv$fhN@XldV=tqDCYxN&1+`AJ2>JeS6?92&8h!TuRx<}dce~yH(5h5g})wI?k;oJ z9#RFCLtQz5w-F~T|1dD7di3s=|Dux_?;4wkTl9;rt~G9qbdqRw?;%Dgb{!-y4ht{- zQrMVITDaYg-fDTENosskg~|KCF0-XP#3T60EO={I{#HM_&!9(>+9BP>ozJ9e?x;6u z_x(@Xhdv0(x(DoTRx49G1Jvfe{%!RSW5*Un{iK230Et^BAxUWvwMU0}g+jr#cY9E@ z;IBOzq3r?p9oe3>IP1&^nNt6Pu9eWmbkG1hY&Ryw@9df{C?%hB;7922es?udTFe>j%NomEd{E>H5S9d{ zPv7L9*LH|^oSV(r9yV+SY&iq6ZtISzHtr5Kdt7z%^3TmWL?isA7sZ&CH*1NE{NAj| z6&c|2ybic9lrv8Q6?cr!i#gug-YelY4Fm3usWx$2M<*VEy_`Yp0mi}pK4CqX3&Koj z>P#kJu-5*xVfy+pyNA@Hrv)wywKNKx+YtYqZ*sGw{{K&${4We(2#}Z;LV>3LqS;fP zZF!l;dD#3$X{83;_qs9{OR_KtxZAgrY>X#1HA@3khwPH!Y|s01USCq5BfXx)@-l z3a8%SRQIy3ZFjL3^s}wj0N&ZksZv)^KWUCm9z3XBsa?9mPneFkioQkT!`$-3=w1Pw zm>OA`vC;@i3bYUJ{$c!)h$>?pIF-;EspcEzk*13n;+b|p2}>#J2((bbN4%{)Uw&ZI zi`0;z^f8o6W7OWo3+(sVrA}AhdNc55!X3&(WH7)+ug0X0h09%ohpnu0gZJ9%1S$XH zxwU1#klcrDZ$(QD;q~}04`eOc{xn=^_h!9NTWGvA+<>LgAs3scoz>KH=N_PwwHNkWpA?SrO5qFyyovL4D`r);H z3v&oEHMNjq)K?n>lC&!PxJ9wO?%=TVP0;-nV_}MDU&NcQ<@Za$uv(*AS387Sm4C>( zaipJK3$o`ZVA=q-rMv%mColalFXo*mI!PyV+lQazR%8pVFrM;nIL)8T#VYVxi#q3CV4SnKn|?hlVOIU9#}gy#KzAhU6K7zhy# z11)$TM_d4paTjrE)Yhj15j|CYOt`72mZC?I@S{W2@a>yd%P)>lleb&a+X0knryhLM zOX##=vHuHn>q;JEnKbS(z#Y@${3&a1JUKJZN#G4JQ=bgA$@<;Np{69VFa30r0OEuzu7@`!A)FL#rN+Ba7 z#fFg}-?;X$8zG^F)LMN^IVfQTq2$p4Hy}y2ybJFuKUB`~xKw%BD|xDoiJ5^Wi{Prx zphWwG-!#qY(_`a;7Li*#dl{obWZe91Aw}tXZnD0(EkEUrziwWG9HK^_6R}u5Mw(;s z>Wf8JmC?Q|p{EXkkZKx_cu1?HE~I0Zn9jecHRT`-F@E0Uz-^)yVV2aA1qxF&4t3v) zNTg2F?~;wlikz{)axrF;r(EQFX28f@sC-`Z)I>j|q4^f;f%kV3 z6|(p(pu^31beyObF*!qe95VqSB)pA!iU#H`V;HziqcQ~0M2S5yv2Swr0G3n(S_obMp#2W^2! z~vjGs`{YMUu-w`GOhZqobW?_4Endx(qz4ojgZqD&F!UjJQq9nQWl>Vtp-NMsG)h~hk$16yc2$E=g2kARx9da5n)L^snymtxfi_(^&;9kzKn;M}(|mayS-INMN3&VFBR80u2Bz#%rVaGn zN{daPWJ5>CPx(=FxE9T1L*%2s!wrXD9s)n5*^y&8gjsC4{VtSchh+@#_!^sPYZVG# zuDiW@3^0FXs)|WBq`U~@V5Tv|-D8m0L`>Hp^qRYH=es_G$EjPb| z${Xn5?G}0b5@lJ{iml(L zJ1Qgegx$cq`cWRbo2gMtzn!~36&~5c63lYNQbbr3W}o%7(m&bxQzURN`Dq@|k2H7; zLEm5&=oe3S*9jT{$>|3am7$V4c4yw!UtAJxP!cj_vxy%@Gf6q1k}% z(LaO@Qql@;I!!B4mN{Py-mjWi0e%~g_5H-gI4>#s4CyCOQ}BO>7Q1c`&gjhEDUty> zbqnkOS^zGNO(*a4N=4_gNklxvdd_R*i4?E9>Mp3i6va!lBHKprHB1F)RAVawMFvut zH;sYE`Nfa8i(Q3qV7b4~WLN;ut;ffVTjDs-(ptZQ&Gk1@dIObx&izTDz zKSK2!es8uPhnrP$lILt@8}W34hxXkb_#H02ftmj>VtT^Gwk{#&%R!d&)I2j}$2SfpHBHS&SnQWtS_z-Z>4Okr_jY|ODIS1`ecXK2{* zBJBPc2A!Icd`GQCxme~e;26)WqQO+24M%uh4aD~~JOAnJrtgKzj%|I`dZ*Y~Vhk>R z|7mZABwUW8RKr`peqX;bNx~zKEeYKqY~U=TtN^?gf*z}u^06v)IdY}o$oXrZyIiba zAK;G#`k^n_{NJp=#K2x=>e#>{TFKE-OZfDbFLu@G2%18%7e8LQB4l~IbqWwbpm~QY zJYb_$HzQ8V>i8epmXP72lc=iYgpY%Mhh}HQ^2>pFG`=>*j+OHY(f3!nx=@PP@{p$P zWT2U}-?fC9PGBrh$8-ltg1m^>>aq1>Dnox{XV;76Eq6fI*(WCOb{BQTIko?z$O5wD zXtU#ZJplih0#~gJ+``xTv+M%*l~0_0N#5Lck13Lz_+)e7ABOz?sGwU_tC9SE>oqYw zvxb3TmD;xRf-X1_$qB>GFL|sz;D*Mwxu?l?)6i+AKlMo5$V2z2!XBgU@VBUcbe)|8 z+ax@?W=_O!@3uO-jFL|R5AGytJ=V}VyZYETso>0!g1sZOjDEe^YpEJGgADf)aZ{Jw4#DEU9l87om zDZ2tCjy6;O_orEc{eVBO1oXh285>urCgE!ycJ_>NF@Ew`GKI4I#my9P+SsgD`NAEv zWdjf2EbmMY6_eTM_9VLa73S+^bxLxqDDO9doO)Eg<+~=YJ#y#MTg5I9S^60Zt2O~W z@$mZj)4DyJYv)VSBv*eFDXP0os;oTkqrKrX(ULl^3Y*0p~3aOxxxMSc(WBadP(HthxqGvYoszGB5ZK7UYlEe$DxTd)7uDBf6 zV>v(cFP8}VdGy|h#S&&!TujX+pV9mFs+)j#j(G;5WRaW!Oz~Qx@OxL?y*F_d=c&Y# zzV8>m4!wkVQG#*$6!N|#RuCjAa54RmlAqd36_0s6Own2|n$ExPuO~C)C`By~A8wq(L@-}bTE4bcf;R#UQ z;}~Q(NNdMAuvsIYt}V^a(ykhmyVe5*L33mwFsBX~&&DQ`?rDqF zPl9Al;K7hO{N`B(-xu&>WszT7%^wxC1EySMvU;pi^&ZRy6vp)+*SViH`(wl*+8J!&)UqL9B!@FVYY1cs*?Sy*< z*WzC=XKe6sYgnLw$LQ(_&#&yR=k?FoZRT~Cdn~FJeEQl)v6h$N?G5o3ws<*jN{cwuuko7r<0fl7AbPr0T2l2`ESPl3^c2Z& zM{D+}S@^oE(SeZpu_{Y8PaUr4u$2-NaOC^KWvGt(ZVwllv-T6 zQ`s+aVQ!EX5_i!7&j#Gozf+U*A3`aN>P85dxCu20_+YBim~uL;3$z5s5@lxTn|yXg zr%ik0bi(w^Fmp?Jnb)GZ|1oHK%JUn1V~Z4+lKhOh96y4{VCRiELblerqUsquuOBR99|Ep{m-mVIWyB!akuGcmvfZ0YV5n8yc%RA7=OXr)(h80 z@|TcSn2q)NVb3bNdR9E7%qSBz#l|m%qb4M>KbBec`76u>Q>|M>uFS>49C@%q*j$zw z<&p? z${Z)39RD_m9V{qjzP;6{YmID#9#Dc=Z3bKLEfeHz@kFs@+x5_c3+k(rJI`DByZ=pA z(D<19ClK#y_W?Y;v*i3@KB5KCG+4$Ic1U#$W1}LjKrZk4G2t-^p&)k@V)BVc-48p2 z3bUv>HJ$p#F0BR5fjhImxI6jQPqK&~=(408#ZJ=`a;k;Wz+a#{>p;>swr4%GDk2}1 zA0Sr}N0~he$S?n+A;|8E)&D;P7?EB0)k_XG%J4p(c% zjVrm`2bw$?JV~4b!zHvv5qjN_^kM>0(?ji?Rt=f2e#}y(t-M?AK1f?8TH9>$embI@Fz+x%-UpzcL3!dn!fRX9OO_nhEd33q*hO zLZ@Fc|F;tT%0ki0H-n_EdzEW4--9jchT7_HlH*p4*d-eF@5&{|)m^&;M% z&9WQ%mCAbIzWK&ov7SmAlWM<7SFFkyYm(5-X@z+{5lTtX!1gI0n@~kGGf}pDfMjQd z3EybBG1){PvqOV1>^-H=yBngD)%(M8!h^+uC;H(nC6fNi{069x)~w&TminuRkg%b+gQKnmY#I@oK|0^TZUkc35ye<|*&~;tg zXKPXPNT4{7{`^i6x45%R73W>2eG{Rcy3w;%Pn~(b0__8f`(rti|1cuajj!48yC+Lj zx)|?Se%bh37x2QhOK%Q2hX{Tw7~a;=+sL~~F{=Fopv(SRZ4bZuR%LnICLP&*+D9IQY0Me+C5xlsi)lWc!wuJ6OaB3%Ci-f3mctM%PhIILPsqQctWa~qadb;x>L!5 zVt6T&@#nN^_54PK)G+s1;&Cx=Aic!A$DEToY%(IsPoacO{Z+P#;r8t?DZLKezK=wt`!6*(KO||f3ZE8>cbRM7+zN!4V z`)3p+*io{HuCukEB8u}`32-KQ_NxxUw>uf}Q6i<{3B>XAy_zjKSNyNa0&h=GFlj;O3@TvHT%`*}R+w%c3A=~?xXa3g+KO*Z@}f7dhV6`5a4v zFc>-7kghgZs7xxR>L;dc1FE*3V7=##cEi*!qv^G0opB3ZXT6|FRgvyw0MODpm$onQ z1Pin6Y0m47Rfq8QVYSjfUyM#FpE?pv$`Zk%7p-Ec9}eu0G>JCkgHrb6x}dpu8#p#_@936U#twwSyu zcc?CEc>dorB~lNH3`E55DxrYR&Y0*SM)op4vnYG|SG%@8GX@_RabRJ*#7XQke~;rj z89Al|{sVYh6bLC0$*^LZ+GrAcD`HeoEwQjNX9B*jj5`H&r=}(flg#wmcFNFZrt@0O z>dS-7lduL1IZYdtGa(@QN8c}f!AI8b{gjYWtvbWe^Yi>4`Od*dAmWqXk zTb$}@cyvatB=J$}g7@e-=Cgp}E=I2$fsXD;U66;iN{8lKEm5yu`p5d=Opo>{LzZ`f z@xpTAd9sFy4ik(bQtKP?BF`N^P^aS^>1To5WUQ7UqI3j?1uCW9E@LZp9g9Q{$C!rp zZza*MNvFcp*u&2Jw;#?t4D4cy)7U2@>UYtU)Zs|3k{q9UpX7pZuP29o-fTUx@{tXZv% z^QC;LLDD0VsLBIDvjVb=&WQquiq7)^M0&^Se4ZDjO3=D561MblD#JIJ2#0k=gTMT| z=tcI>)2P`mW8%MH5P?*T27a;EkBc*{7i;0?bBdp_n5fOG=I!zxZ z2_&KxL?Oh+6hCD0g8YCe;Ep0Xs&18o#Db3TL-nS2#Z##O*=W%e_;u>-wRES_KLEUVQiQmTKbp65M-8=hR zYIKi#yl2dP53JVV9>|UG)9*-Mw&`<$g}VCV>%WfwPl5j$1piH>_6LxsuO3eQ1GBnG zVJHurlud<>M&p1s{7zvg(XNG~%{$~PR1A7$%*nsw*K*`|%0cE?>$cbbdl^fCnS#b;0f&X`pT0<%z^V8|p&1{g%LT2|WVsdk>dpBJX$9W9`a(^#;DkW4v z^MG(gWopl$ig%A!BJYq)CO!54Fj@elK;`@*iT3;owFUX2z|`2Kv#oCOr7p*yi&g&x z;yI-B@)NGZZ0!Zxb=SN6u?V_FG5;uL4MC?gpH)4GUf#jftM*EZAb zuaNcYM+gw^nt0do-1z~r_i^h4m0#os>JTyx5!XZE;ZvCub_NQX$=`d}VP>g|#d)#WE^|Vr5it6tAdH?PTI(o|cjPze$ z|10V^@*)`mbrU=teeBVI;V_ZrDd;t}ZY}g^_aWr#N6&9O^52V})uLr1v=oSs>9>Rc z){P?~+8}3FK2L3^nL9J2+IhYSN-Pe3vXXDtm4DXL^bfC79)Cn}lfmFjhu!1UN9ef(D)Y1sdX(=PLb`;Q3XtI>xiFEoqKtP%l5dWx6L zqnGRz$N%pweDx3GB>OfWy+>ZuJK92DemgOZd-Uqxn5nIM%|AJ}D2MJuZoCGexL_LMjIDIui&|Rx>c7BUb z75B(cUyGyPF}m#rIIs+i0?OS+UD z%uV(pP_NcqOesj*vJ7tz{@T@3{9^SnpnJ%xtEZEJDDbinf~JF_cPk6x-D(3r{axL& zsXN6eHWxLd0!JooQOo&e>Hjd8sP}o#-<*6vHNk;_WuCD_{aXg%hxMjmI(Cj z7JvDcC;tFgP!NCiM16N}(<5J}dnlj!q5lBE9=H#8{{YKgScP`^^3S0JML{ zXJ_eN)Mjfdm{Ep&f7j=i`hVts*YKZR57%V%KBuYbeNR)@NH6jI>9hNH{&YXcSFk-0 zW6(WA*1uEJ;_%`r7H#|y%Lx2#c@{h0p% z5BsIArXSQjpD+0PPjYMUzFY;W{KS8(E=T>9bj^mAa$QPG|KY)-tS{kC|h zb85wstoC}>z1d~ltuo`TN>{C>%dn*ALvaB{&-f+dq`ipj*~lgD55*eZb} ziCmARMmIS0BmRzry4GrAiP>UlY83fO%w;^bPHc$3h{wiEUKZlPsT@lmJ&KJ^wb>@b zO>@a*4M9qn(daJmFF0&C^Pj~vq-SwMk^w&P>r7V-yF#-$Gly@^_!9|I) z4c*$Lizh*zu)&Y%^s!`^pQ?k%WzFcKH!Se1?g!rSLa$9m?jS`HtHvRuqN>dNigDN} zK~T#!uHB`{-J<(^B)7bC?d0TgZeBrW2+c&95c=AIt~QMX^v>$p+=4O6A)M(;uLe@Ud>?x z6Rf4zvVp)durO7c#xr*WJ4Rh+v60&o6-qrw3#bU&BlwnY9z1yR71ruPR{=zmGum}U zWvRqCl_?Nr=hMq4GS7t@mhRD9C8=&rs(Pv(I0K09ly-)sz)9(X7UM=5r52B?r5MJY ziuQE`6cf?FLK%;an42kkh|YR*)1=t;1EtDT4h2}%M7rCXD|NC&yNstNxJM>TvZTbs z(gz=>%TLc7e|%O_ZFAjqM4?eO<4&sNX4Z)U{{Xo*MJ0;Tvu2$DW(--1yZoSJn7zhH z>SD?DvChkwITsBR+Yz;G1jh=~j;ObAJw~tYIGRJP{K6FcVGX8(-RMw?1|$}V=gDBW zg4l)cqe+rdz;u}o%(ZrW=iqGGq9^tGYEck#<;?a$nWzq^ju6KY#&J><#hjhrwDmMj zGbE4yxTROLs-g_IlV(M{ zoGZC3S1Q4hMof8SbtfZ)d}%5Kf(?uzCOBrSNr;N#6Iec%V8TDmaLGEx?lDi=vb>^T z^(N9MiCQ8h!}Q8>P73J6gAns+&4iw03nIF5Rd}&j-fNr!r^P?3XcIWN!cmRnofvj0 zT7!fmJ1Cm#hjjYzwTf!~kvX}UBiREP3vF*yL(`28XLgr@YE*VyMdU|n$XK%Tc3xq_dI*uoeW_fm=@gS6Vj-{U%6CzQj&b^o3K+0*xVVA+< z26Rxw@xUzK3d?zanaI{l=8~aE)FGrNyX_i2T_UC06X1B!d55?63nRykqmIZf3wG(& ze{Bw?KR<;>3v1DBIE5h4!i_o6OxL7_6Q2~LPJ8~?O(b6KZ=|o4}EB;kM8ilF%0}9DG229v@G4T_tQ*dLA;7;9s?v~Arm@(X29Cc!7z%fyts9Q9GMBX1v56cjC9Oh&0q zzm*RJLolmk0`K!!Z;iggu;M0SURX{sRH);TMGtO3+N`lR9A#BdZ8H7s_z{*4y_r_= zU+vuNYa|S72^z-my{Y`Ad`RO%X)`N8zDs@(T*$2!1+bca7ricgwMXSO*-< zV_pfhLQ{=Rc^~$=;X59eMx?8)1o;KkBDCXCn*=(uD;+)+4kPH~5lpb8%9K#z=~9_~ zXrN(zQew6o5>7+hV(EuPliXM?NSTAIbuo;I$Y1qq5i>p~l*4U{xNVS%SMwz$5YAO3 z0i-Q%(q^QV8P%j;U-t)U*$`9RdUQ9Lv5MHbq40J$t0)s;u!Sydj&b2W;&b82H!<40 z*C{#%5VXug(OWg(W?~|C#yYeA02J(AEXxN@GnB1ULt)ny(;-ylLozhAZBPzxZlg5d zT{Fn3buiyNe76v<%xxJav{Ka!Lb42a^4fJ7@s7y7!mY$}L}TL5QY9K0bs{SrNt}d4UjPsoIyytn}~1M{!!rIMfqUPDC|PR;!&VR&;*8LRIfOEtbo;oJ|DO zg&V(1^Sm{Hc7<6-o1$D5eyV{ekw`GxIN3(;mtVbEV%hXH^z2p5Y+fr`Ey{T z+Y^HceIxJ$c&3LAX77u>Jh6-^R;D~UYVf>BL{`FRT79l2P`d7BsX|&Dpv|KfNR!C$ zZ6_;Il8EHueNtl_0(&yF@Yz3te^Uw=@y<-D6mgYQYg3n7tf-lAu7pH@zqGeTJ7dhA z)-|H(Smgq@K&d+hiHMWRf4>mYv*a|UgNZ>x1lj94)$Li;`hz&9TD2uMDgxj)Qv~r2 zSe|TH*cq~9sys90}xTN?SL>{cpyi{@4 zUOmz=?gW}CZFs7@ecKF{1)W1LZ|%{PnjM*#6+qer2Z~lq+@P#y$_fX9ps5v3xfL!* z(7z+elX-2B;hbS%#v%$kl?G20j?GMg3caW=HEF_>u5Ffh>db^R%}_MiU85|4F=bTj zq*qP0QAPM>RA33_nH$ZL4wd{(w*@t|YgCD>if=4-u(@jbQZbpD;m4z~m`5(t;aJj; zJcjy_&fVNhw!e}Liy`Lhf&v_+Nj<8WhiD}X+gicL?c1`JO4TD8 zJgsISJI`XGN=;=20ens5Q-2k4%4BWI`SO7_cY?pA_C7@T=0 zsF_TCTBCLBZsep`o)W-3uUIM5&fmEl|G*>;kHv zxKk-+xds<)W(*xx@f56@BFsvToFx_!Wa@EE5~xyh(M_~?W-i?Qa}zP zhETH%e0&+d5^aY*Y{A;%b*^gN=$cYRP)%Z%nu^_bkWNJ6nav0(m5AJf%fH9cxqO^* zh4DP|R{l_f)5l89!Q3-ESbgXxRQhZDXz z%@|5s;`nllw2wO4Ewp>0PXb!?rJQ-n#WOh$RjBfMVw~LUepX9y0e0V#-KO)C2st%c z(SqQfKD>9DqLyDNv#jTY*wo5!lTyG?-f-%XNn;2wD;G6=GlmX^I?vdA=>vLS`687G2Xsx=+Xp2amy{%*g z-Ng`(jR+VeQ84PqIGOiYh9}gLnqTi5DRx9|0N-lLIWCoacNaMGEcp<^G-twjsZ|YBLW#d8Eu?&5wA| zR5wBmk1b;I$R8;@!FxdCBk4gIStl+vhcsl!OEIR-%s%l4_aHz@#%)_S9sME9RnI1B zDM~fx#p6nRY;}rCR`{MwS8FgOuu7?A$BU`;UL{O!asL1e&GI;@2{B6>2ds}H$3Pve$P7CbuMRq`RR|LkS6(qd z(*dX)JW*8=EPk>tcB-=f05K#GQcG zhrPo^Y?(z!qi{DaSq?&qJ*-cXZ8-`C>tckpEmto4Da#E*A1y&AGcUaNe$DiKrU=Q4 zTiixb7WkbEC;{=|t(n|Tqed5--ekd&aiq5z@Xd=1d_;E{98UXp)rs)5$e&!D(UB!R z=Re%FOF9M82HAeJ>T0k1y#D}?fV`2};C1@_SjUff=^9XprMD{zCb7xP)L%pB6ZNTOpeSMSg1R)6Ec`8 z#J=Q~eVJJy`+2r3-E3!PD(yVQ*lex%OOXh1`sh-gjHt~+DJ|gjund4~$GFej;l)nH%?+{Lnw~J6R zRD4fq^E}3Y)vjbn?3%L)%E;SWNhJ%)nPHFNN)}~R&%&~Y%a2&fkf7q#Mn_thEgKJt z?R2?IJeDEIMR6`59p!@vmctW)IS#!hlYxRUM~(V8AePD)T?@ADFEy>WW` zKRDf@^9M;9emi5N@#b+jqb%tt)zd_t!L@5(?AZkE{aM_8Wn-9R#OlPC3uRo`o39@$ zKsjf^QY)2IBv&z06V^s-;w>7acdz%PzbE@s0jF zl^c#_!JifLJc}}}%+iyjlaA_4p34wj)D<+d56AwiP3fzGjjGS#QP)fMtVUYmHJa8J z=3*+Ksj*L!>}qVzQLnr^9zc_JdMN|la%lviuH5EQD6y|uGje-a%TJp+e==K%!Wx&SDv^*t}(xO((Tbq^X!jO_XFV@?`fr#Bkx0ScM&<#YAq?5urS4bRf@j zs6MFG3abdsW(?OF(c})B@uuucHPCTmkWotoubL;2WK85qOz(c;e8Wff?jj;D=6O+3 zym}cY;Muk!arNdhsi~Ev`>kCpe=2Hj_cIaQDUDEIB-7*AgIco9)MJhKR*Y1M&>Btu z0CgWKQ{brCv_YL4&5}u2vEiH~%O83v=R?+1kGm6G#U_-#J-fua#bJDsg8brUe*RJm97(K9!YnxrYUvj zGNbT7YCBkba^B#A{az$-+F?nw%iEP_5Nq+Yf&1WZD*x7^dUd?7_+I;8&q z4rI|>h2MigpG-O9C%<4>rY(#Ejq9>i2w|+g2jU`M}{{Utbf)zO8BbFM) z7KipTO|3;>%d9J1Qq#}2p&|A^o2`}VV$q_D*?R-BCE8^J5X`LD)zXtqmv?4%LB(!5 zLmiw^2q2MLr{1*%L04fEel<33Lou_7PVyP?kPO#xep9|?9}fo>ix5|)(#&4YN#s{# zZjLuwY&IwyHDuqqnVXOc+c24<0tZI0#k4>7o z(y7P|BSoUHZI_5ysVRdaNd77Hsah*J-?0(CN`ZNd82V`uj?vmearEy?rd@QQg$^Rn z^+*tuc54jc-a&?S$NH<~a*1+ZZOkaz2FMXUvTJVagQ|-?`_<-wz^@_rg{9hiYRLW> z0jt@c9SLcT&rL%y?fZ6)nyxtfza1#W*wk2yn`%j`1c^Fk^OQ)4g!L+7CSzg=5gpQT z6He3jsSwnAXy>qUWiW*dxf2gm>0U=9xFK#>@k#2}{XK9#Hj5Xx#1>b(m$dV{!R&hgOn>wbSLpX8k8_fqFHrs}Xj|F}`k9DZ6DH8<{%z6%Z@MA}Pq&eKu z_El6M*Y6Q;n5T)z?3ExkXGyCp)fA?6EhPaavq_Pdn|gBF$@W>M~<~=2b|`#F6vB$T4=;J=)HQe+j7)qb|2Wp zzzb6EYmfn`OP*W~0cN-Y3rxKY1jAS_@GDgIwyBN`v8?by`UwcOtTN$kS zG*oQt1SY11*%3o8LeI=US{o||XPINFqZcYX0WW!zwC&DEZaWa{t0fvXrOqzc(};Ry znixHi)XT;(ri z6~~Zg8Bxm~DdX{A_{)uCQumKkD%@x0wz-2@h`Fr;6-v+|60*aKY81UYP_xj%qLOdS zMkwbXDOKaDt{GLDftNj>K`{tqc=5(Hl#;G&;_|Gb0JDBpUDmY|?mnTtS zr6?of5u^yUN{)>IfMx7XP>W6mB5{M3TE*a}j^?_V@}4lFIL`RE0WZkP?x|Bea=ML} ziQ;ibK}R7Q_TdVqfmPMoq46wXbsDVO8p(*^smHGRQer7&x4q9f0(i+DM=K2!3Ng*G ztai)oPtz_EiVP%vYkd7S2R$p&PRisXg?ngr$5NLS3QCDokChZpvhD+M9OQ{8p3?NF zK3T^O=s68!V9bL};IAq6A{wP1;%Z1<-4iCMm2G@pWRrPA+q>g6+}?IosDxKY@yj6O zb*7?R-;&-@lP~h1&Nq%8(I#%Bb#)@nXXZ)dxcuu|0*NMepfj>|OwC04XZ`#)ExSrrIISEQ_qIj>Mo(m?rymU8bqe%|GsKdc1;fnLXi)9i6U4 zchH!Y=PY;o{g+Q}-6*Qa>mtl*3-%O}SO()2N*O9xt1+`(#>%X}FXL8S)S?$!_#olO z)A^NSd4*TxpjNZ6Qy|Tk#q}+b z!dU&LM57ub{U+A66BvsmNjux5ab)7vg3%cK% zPcn?XP|%lES}0_st9k~k!m6$)2v$lkU2@rC6D$qktdR@pnW^Ki6hq`^hib+?Ry^s- z#Hw)eIqK??)WB25B*AKBlx8)&cKb%YI$K{Ni71ejs>;>mOCV`kQ>9uIbd6KBkTC-x zgAG|%$w$@VlbmG9gqX%^yU4AV-;pF#*rIY8&pIw%m?z zklb^hOKv(Wg0V%RFBLUn^<$I7<2pS#EM{ldu0oYpW^w&+%f>QedUnPHuI6J>(7mip ztCsaqVpj`ma$s-=Y>_j?MO4P?{{UH|_}95c6kk;xpX9_ zkLdhgwO@TbFPHaw*q+wl@_19_`xN?@u6mEBd!yPl^f7T>O2ljxP0yhJBMlVIGo-W zFONm17nd$X=Sk9Ru8%P2RH!S;OhSZFgj8}r`TqdV{{TaciI|<^>|ig#n5i&$v1@)oB<4sR@AU6_ zJ=e4L@bt6Vo&NyGUEzK!xT&pgh{U_kAn9U^C)kz@meZH(d`$adv18(R&d!xM!8YJz%F?nv zNZVlg5*Cv(SR}&!?h8_sYrO4Gf%3P0K0dt_{xW?t(*0-rh5n0u zH`cv3)BR)9IbW$Cw9Cr$-&Ns5)V)8KIs3cQY_sKXc`|h5%uyuTvY(Y7^4IC#swf|( z`{h0-6W`<7z5f9BeJj|+FL~`sHYuOIe=prUPyEY?@}54wsm#HC(^W8zp_Vk0QKy%7 zigg7prHrywSwU6|%*}>AS1>W>#xbm!948JG@{)Q-UVNn6Mpq#GW29qVkqlxbdviH6 zDW>hI;B>oQY>Z@bVv_1))az5P0yD(InKJU8RZI4keZvS&vl~T%nT9ABfUmOZfT(?3 zh$F^v$dok;m;?6%d#L$_2{bWN4EjWjj@C9si(}*|Y4S(8Mhwf+iH4bxJ}~oIZDU32 zD=o6m*~gzGMOrMK@MrvUD+b4s<#_?Kw(CL0kGB}G%9`Zd%;?N7+uOe$el(7jZ^Y6k z($I6Ke6CQG3}1xTcS7S?I1+;J=9D8}@$RG1a<;H@&IhElb|!6sh}6n_lxO zBV7=FyVtIp>-skT0Mp~?{GJE+r~3iPxq|V%W87bSeNsdhXFN_n-d}wBUi^-+wAy_C z07$Fq6zw(nbyfrG&OeDdbNZjSDtDqJ48+v{57em4t4!g;t?x>buP;`-D!pQD)mT*>e8?@;}1; zb?jc>CKj7gr)*{9bry}g#$HO@;u&RW$ALb%S@zOxmlhuKYK%lht=(9YWIN4dDLU0m zR(2u)Jln3;W$oj*vQDMYE!J&HC5aqX7E|QRsjtgya`dv{C#R1f!->y{YZr>-T&9Xg zPGjjxNx_Oo7*u2v86z>SHOjoubzxs?n$)En5-+OX@LH__` z{m5(f@9o#FL)3pz9=+*2#{Iv%{^NRS!YewR3k{Yy6O?!To9IYdnEU)^(7LK5Q=l}d7Btp5NI{-={7{{YdUYaSo!SK(xd zo$69gQ8VK$ch|*r9ag@(Z?3KDyiff#-=Gh8{ha%8?yuID+^_s+(mDSCs6S`%{j2u7 z*<$>@3}5Y1dfx}t-`}3M!pf~V{;%qum&boaW=zY{cs1nCw1=(!A3Odp{ZwShkJ$Tt zWXbJv7PPzCJk3uo#$e8`CibDwXS#lY?PuBhXZnN8aldg8K}%6h+EV`MP|(M7+3qZ7 z*J$SCiV&59S*oP!10tA$A~ za1y8Gz#-LJR(C>Ef2ug~gIyXL64Df;#~phuH;v>35Ip7vRh!2tvb$xyA($D% zkj&(0lQtOQI+I3UKO~(@A<2=!LYEB3PESwkPPJL?qavx8u5G?gG^5T1!^yj90iY*V%c>0#0*|QzWtfCLzq!p3Hkxaq6`UI#B^k_i${H;maB=4(qWc)=-EK(LJ~@=~|f@BXNOr}P2#dVy7qKUg1cSkV>GEr`N-ZCXsZ^Ffv`<67uSxaL{d;%x@-g@O?03T-{p=rXz7$}e{#`v2 z;D6*}^q+SVKG!@)&rSaT@GnpH-~6-f{{S9i{{V(Pe_F-SVmToO9N2Ou1^XP8H8O5G zjCewf$mjWI52#!>*MzJZgEPHAXcTX9J;;j-$nBM^i2s(x9){ai1$%}te+st(5 zf|<8_6+EQmV9iZ816*PzJx)g+Zl|$vkh$aq{l_8ZSC9$NP4h%)*;Im~L=JJH;cMI{ zkg<&)Y-TwA38x+hxQ?e4VyN2ZF8Xs4lhSx%r0+y**HB;>wI(8M7KaO$AI(Rrm=(g% zG@rAiQ6FKsaM1{&Bru&GW8|ubqpD15sIxRrwdzcmwu(MP^x_R-Zo_w{HRV-XsFt8+ zA~Tnb#mH`uba`$YQ~BHHzbS!t%JVh39IF1|pyqau8KFR~b)t5v$!T%;)auIbBIS2% z;TAy|aTq5qdl4t@3et;&xqaO`7@ALsjmKg*!p2TDBq*@_BT#7pnyo3b;RxBt)Kh9j z+kHgnD{7Y$(HNO=v?UfoOkby<$1J3${9Tnotb$5Y29m!TvxSXl$V+jFZpGQ$>Nwv4=9{QM zr#q@>{{SJ-C<0+fm+*hY6?&nnJ1GNU*#c3JJULXQagHVnQEYMI zzAC6DCtiG@y2dH2Pg}qGpy+A(1^Y>g2bld}{l0ZjPEb?quj&DnTQXC2>(|MGxc>l{ zW6gU*|!?iTZC9vq}D^OYlHk|sFX zv2V7~Q`~VgGtm$0)jfIdue(3R=12Jz{S_5BVfvB#R8_JURzB+V?OS5NDBDwI<1x9VQTf4))vUzhFw0Pugze2?`E=|c0fpe*Hv z>q|nzFqFdU6=!D%B*gj6nV0wu#=Zig@q?mZlWxw?QZqJ}WnE52Ys_Wo#W}tM*YT%_ z^yL{C<~1=@9&rmrMM;+vGGanR25cx$<0?Ab<&sGvsINR)mu!hG+R+ZC~{2? zlWe3#h6HX;PI33taT^PN-(A5fHr0R%Bs9Gekn$`gL3;vnMPU-`x4cReUSbzf(%LqK z@VdwU0Mu*#K)pxopW6>c{TKbM^_u!;+&{j4gMVB5zv`Zy%4qtBpnaZu^L-+I+ne#~ z4<4TA=f~AJ*Z4<1k?8#RawFI3s8_##1OAqM-b4DXOlSFDZb`%C-y(x49r38&;b>D- zQZ~0y{wn=iabU;n@aX%Gba#VCZU*1t>izf<+kQ}pjd z^iNLb`p>NT$3Khdev#=Om&N3IhpGCfsrrW^d{0mG?@r)xCiUlY@5;1g7|9xyr4

    R#OR54wM^e^&Lu&-GYv{Y!!R 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

    iCGyJ^G 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 *{sjmWybGI{zbsdSB(T2)>_7DhWSg}9Dx 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 T|Z6CSeRgY3%C{o3Z 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

    5;P;_a|2ii*Hqmr)%C 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 diff --git a/benchmarks/flowertune-llm/_static/flower_llm.png b/benchmarks/flowertune-llm/_static/flower_llm.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a0ba3bf30e641f31293e102a4c2eaef36eef87 GIT binary patch literal 119787 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
    U6lCb#x4Y|Fg9CQ_+3>RJ#!)(1_w1z^_g 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 literal 0 HcmV?d00001 diff --git a/benchmarks/flowertune-llm/evaluation/README.md b/benchmarks/flowertune-llm/evaluation/README.md new file mode 100644 index 000000000000..1b6383df296a --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/README.md @@ -0,0 +1,46 @@ +# FlowerTune LLM Evaluation + +This directory provides various evaluation metrics to assess the quality of your fine-tuned LLMs. +If you are participating [LLM Leaderboard](https://flower.ai/benchmarks/llm-leaderboard), evaluating your fine-tuned LLM is the final step prior to have your submission added to the [LLM Leaderboard](https://flower.ai/benchmarks/llm-leaderboard#how-to-participate). The evaluation scores generated here will be displayed as the definitive values on the LLM Leaderboard. + +## How to run + +Navigate to the directory corresponding to your selected challenge (`general NLP`, `finance`, `medical`, or `code`) and follow the instructions there to execute the evaluation. + +> [!NOTE] +> If you wish to participate in the LLM Leaderboard, you must not modify the evaluation code and should use the exact command provided in the respective directory to run the evaluation. + + +## Baseline results + +The default template generated by `flwr new` (see the [Project Creation Instructions](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm#create-a-new-project)) for each challenge will produce results as follows, which serve as the lower bound on the LLM Leaderboard. + +### General NLP + +| | MT-1 | MT-2 | MT-Avg | +|:--------:|:----:|:----:|:------:| +| MT Score | 5.54 | 5.52 | 5.53 | + +### Finance + +| | FPB | FIQA | TFNS | Avg | +|:-------:|:-----:|:-----:|:-----:|:-----:| +| Acc (%) | 44.55 | 62.50 | 28.77 | 45.27 | + +### Medical + +| | PubMedQA | MedMCQA | MedQA | Avg | +|:-------:|:--------:|:-------:|:-----:|:-----:| +| Acc (%) | 59.00 | 23.69 | 27.10 | 36.60 | + +### Code + +| | MBPP | HumanEval | MultiPL-E (JS) | MultiPL-E (C++) | Avg | +|:----------:|:-----:|:---------:|:--------------:|:---------------:|:-----:| +| Pass@1 (%) | 32.60 | 26.83 | 29.81 | 24.22 | 28.37 | + + +## Make submission on FlowerTune LLM Leaderboard + +If your LLM outperforms the listed benchmarks in any challenge, +we encourage you to submit your code and model to the FlowerTune LLM Leaderboard without hesitation (see the [How-to-participate Instructions](https://flower.ai/benchmarks/llm-leaderboard#how-to-participate)). diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/README.md b/benchmarks/flowertune-llm/evaluation/general-nlp/README.md new file mode 100644 index 000000000000..51c801494f6d --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/README.md @@ -0,0 +1,63 @@ +# Evaluation for General NLP challenge + +We leverage MT-Bench metric provided by [FastChat](https://github.com/lm-sys/FastChat) to evaluate fine-tuned LLMs. +[MT-Bench](https://arxiv.org/abs/2306.05685) represents a comprehensive suite of multi-turn, open-ended questions designed to evaluate chat assistants. +Strong LLMs, such as GPT-4, serve as judges to assess the quality of responses provided by the chat assistants under examination. + +## Environment Setup + +```shell +git clone --depth=1 https://github.com/adap/flower.git && mv flower/benchmarks/flowertune-llm/evaluation/general-nlp ./flowertune-eval-general-nlp && rm -rf flower && cd flowertune-eval-general-nlp +``` + +Create a new Python environment (we recommend Python 3.10), activate it, then install dependencies with: + +```shell +# From a new python environment, run: +pip install -r requirements.txt + +# Log in HuggingFace account +huggingface-cli login +``` + +Download data from [FastChat](https://github.com/lm-sys/FastChat): + +```shell +git clone --depth=1 https://github.com/lm-sys/FastChat.git && cd FastChat && git checkout d561f87b24de197e25e3ddf7e09af93ced8dfe36 && mv fastchat/llm_judge/data ../data && cd .. && rm -rf FastChat +``` + + +## Generate model answers from MT-bench questions + +```bash +python gen_model_answer.py --peft-path=/path/to/fine-tuned-peft-model-dir/ # e.g., ./peft_1 +``` +The answers will be saved to `data/mt_bench/model_answer/[base_model_name].jsonl` in default. + + +## Generate judgments using GPT-4 + +Please follow these [instructions](https://platform.openai.com/docs/quickstart/developer-quickstart) to create a OpenAI API key. +The estimated costs of running this evaluation is approximately USD10. + +> [!NOTE] +> If you changed the base model of your LLM project specify it to the command below via `--model-list`. + +```bash +export OPENAI_API_KEY=XXXXXX # set the OpenAI API key +python gen_judgement.py --model-list Mistral-7B-v0.3 +``` + +The judgments will be saved to `data/mt_bench/model_judgment/gpt-4_single.jsonl` in default. + + +## Show MT-bench scores + +```bash +python show_result.py --model-list Mistral-7B-v0.3 +``` +GPT-4 will give a score on a scale of 10 to the first-turn (MT-1) and second-turn (MT-2) of the conversations, along with an average value as the third score. + +> [!NOTE] +> Please ensure that you provide all **three scores** when submitting to the LLM Leaderboard (see the [`Make Submission`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation#make-submission-on-flowertune-llm-leaderboard) section). + diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/gen_judgement.py b/benchmarks/flowertune-llm/evaluation/general-nlp/gen_judgement.py new file mode 100644 index 000000000000..14ad3c7c6544 --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/gen_judgement.py @@ -0,0 +1,130 @@ +""" +This python file is adapted from https://github.com/lm-sys/FastChat/blob/main/fastchat/llm_judge/gen_judgment.py + +FastChat (https://github.com/lm-sys/FastChat) is licensed under the Apache License, Version 2.0. + +Citation: +@misc{zheng2023judging, + title={Judging LLM-as-a-judge with MT-Bench and Chatbot Arena}, + author={Lianmin Zheng and Wei-Lin Chiang and Ying Sheng and Siyuan Zhuang and Zhanghao Wu + and Yonghao Zhuang and Zi Lin and Zhuohan Li and Dacheng Li and Eric. P Xing and Hao Zhang + and Joseph E. Gonzalez and Ion Stoica}, + year={2023}, + eprint={2306.05685}, + archivePrefix={arXiv}, + primaryClass={cs.CL} +} +""" + +import argparse +import json + +from fastchat.llm_judge.common import ( + NEED_REF_CATS, + check_data, + get_model_list, + load_judge_prompts, + load_model_answers, + load_questions, + play_a_match_single, +) +from fastchat.llm_judge.gen_judgment import make_judge_single, make_match_single +from tqdm import tqdm + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--judge-file", + type=str, + default="data/judge_prompts.jsonl", + help="The file of judge prompts.", + ) + parser.add_argument("--judge-model", type=str, default="gpt-4") + parser.add_argument( + "--model-list", + type=str, + nargs="+", + default=None, + help="A list of models to be evaluated", + ) + args = parser.parse_args() + + question_file = "data/mt_bench/question.jsonl" + answer_dir = "data/mt_bench/model_answer" + ref_answer_dir = "data/mt_bench/reference_answer" + + # Load questions + questions = load_questions(question_file, None, None) + + # Load answers + model_answers = load_model_answers(answer_dir) + ref_answers = load_model_answers(ref_answer_dir) + + # Load judge + judge_prompts = load_judge_prompts(args.judge_file) + + if args.model_list is None: + models = get_model_list(answer_dir) + else: + models = args.model_list + + judges = make_judge_single(args.judge_model, judge_prompts) + play_a_match_func = play_a_match_single + output_file = f"data/mt_bench/model_judgment/{args.judge_model}_single.jsonl" + make_match_func = make_match_single + baseline_model = None + + check_data(questions, model_answers, ref_answers, models, judges) + + question_math = [q for q in questions if q["category"] in NEED_REF_CATS] + question_default = [q for q in questions if q["category"] not in NEED_REF_CATS] + + # Make matches + matches = [] + matches += make_match_func( + question_default, models, model_answers, judges["default"], baseline_model + ) + matches += make_match_func( + question_math, + models, + model_answers, + judges["math"], + baseline_model, + ref_answers, + ) + matches += make_match_func( + question_default, + models, + model_answers, + judges["default-mt"], + baseline_model, + multi_turn=True, + ) + matches += make_match_func( + question_math, + models, + model_answers, + judges["math-mt"], + baseline_model, + ref_answers, + multi_turn=True, + ) + + match_stat = {} + match_stat["bench_name"] = "mt_bench" + match_stat["mode"] = "single" + match_stat["judge"] = args.judge_model + match_stat["baseline"] = baseline_model + match_stat["model_list"] = models + match_stat["total_num_questions"] = len(questions) + match_stat["total_num_matches"] = len(matches) + match_stat["output_path"] = output_file + + # Show match stats and prompt enter to continue + print("Stats:") + print(json.dumps(match_stat, indent=4)) + input("Press Enter to confirm...") + + # Play matches + for match in tqdm(matches): + play_a_match_func(match, output_file=output_file) diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/gen_model_answer.py b/benchmarks/flowertune-llm/evaluation/general-nlp/gen_model_answer.py new file mode 100644 index 000000000000..cefb4fbff08d --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/gen_model_answer.py @@ -0,0 +1,135 @@ +""" +This python file is adapted from https://github.com/lm-sys/FastChat/blob/main/fastchat/llm_judge/gen_model_answer.py + +FastChat (https://github.com/lm-sys/FastChat) is licensed under the Apache License, Version 2.0. + +Citation: +@misc{zheng2023judging, + title={Judging LLM-as-a-judge with MT-Bench and Chatbot Arena}, + author={Lianmin Zheng and Wei-Lin Chiang and Ying Sheng and Siyuan Zhuang and Zhanghao Wu + and Yonghao Zhuang and Zi Lin and Zhuohan Li and Dacheng Li and Eric. P Xing and Hao Zhang + and Joseph E. Gonzalez and Ion Stoica}, + year={2023}, + eprint={2306.05685}, + archivePrefix={arXiv}, + primaryClass={cs.CL} +} +""" + +import argparse +import json +import os +import random +import time + +import torch +from fastchat.conversation import get_conv_template +from fastchat.llm_judge.common import load_questions, temperature_config +from peft import AutoPeftModelForCausalLM, PeftModel +from tqdm import tqdm +from transformers import AutoModelForCausalLM, AutoTokenizer + +parser = argparse.ArgumentParser() +parser.add_argument("--peft-path", type=str, default=None) +parser.add_argument("--template", type=str, default="vicuna_v1.1") +parser.add_argument("--max-new-token", type=int, default=1024) +parser.add_argument("--num-choices", type=int, default=1) +args = parser.parse_args() + +# Load model and tokenizer +model = AutoPeftModelForCausalLM.from_pretrained( + args.peft_path, torch_dtype=torch.float16 +).to("cuda") +base_model = model.peft_config["default"].base_model_name_or_path +tokenizer = AutoTokenizer.from_pretrained(base_model) + +model_name = base_model.split("/")[1] +question_file = f"./data/mt_bench/question.jsonl" +answer_file = f"./data/mt_bench/model_answer/{model_name}.jsonl" + +# Load questions +questions = load_questions(question_file, None, None) +# Random shuffle the questions to balance the loading +random.shuffle(questions) + +# Generate answers +for question in tqdm(questions): + # Set temperature value + temperature = ( + temperature_config[question["category"]] + if question["category"] in temperature_config + else 0.7 + ) + choices = [] + for i in range(args.num_choices): + torch.manual_seed(i) + conv = get_conv_template(args.template) + turns = [] + for j in range(len(question["turns"])): + qs = question["turns"][j] + conv.append_message(conv.roles[0], qs) + conv.append_message(conv.roles[1], None) + prompt = conv.get_prompt() + input_ids = tokenizer([prompt]).input_ids + + do_sample = False if temperature < 1e-4 else True + + # Some models may error out when generating long outputs + try: + output_ids = model.generate( + input_ids=torch.as_tensor(input_ids).cuda(), + do_sample=do_sample, + temperature=temperature, + max_new_tokens=args.max_new_token, + pad_token_id=tokenizer.eos_token_id, + ) + output_ids = ( + output_ids[0] + if model.config.is_encoder_decoder + else output_ids[0][len(input_ids[0]) :] + ) + + # Be consistent with the template's stop_token_ids + if conv.stop_token_ids: + stop_token_ids_index = [ + i + for i, id in enumerate(output_ids) + if id in conv.stop_token_ids + ] + if len(stop_token_ids_index) > 0: + output_ids = output_ids[: stop_token_ids_index[0]] + + output = tokenizer.decode( + output_ids, + spaces_between_special_tokens=False, + ) + if conv.stop_str and output.find(conv.stop_str) > 0: + output = output[: output.find(conv.stop_str)] + for special_token in tokenizer.special_tokens_map.values(): + if isinstance(special_token, list): + for special_tok in special_token: + output = output.replace(special_tok, "") + else: + output = output.replace(special_token, "") + output = output.strip() + + if conv.name == "xgen" and output.startswith("Assistant:"): + output = output.replace("Assistant:", "", 1).strip() + except RuntimeError as e: + print("ERROR question ID: ", question["question_id"]) + output = "ERROR" + + conv.update_last_message(output) + turns.append(output) + choices.append({"index": i, "turns": turns}) + + # Dump answers + os.makedirs(os.path.dirname(answer_file), exist_ok=True) + with open(os.path.expanduser(answer_file), "a") as fout: + ans_json = { + "question_id": question["question_id"], + "model_id": model_name, + "choices": choices, + "tstamp": time.time(), + } + fout.write(json.dumps(ans_json) + "\n") diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/requirements.txt b/benchmarks/flowertune-llm/evaluation/general-nlp/requirements.txt new file mode 100644 index 000000000000..7a0f43b98698 --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/requirements.txt @@ -0,0 +1,6 @@ +peft==0.6.2 +sentencepiece==0.2.0 +protobuf==5.27.1 +fschat[model_worker,webui]==0.2.35 +openai==0.28.0 +anthropic==0.18.1 diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/show_result.py b/benchmarks/flowertune-llm/evaluation/general-nlp/show_result.py new file mode 100644 index 000000000000..6a00c10bbdba --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/show_result.py @@ -0,0 +1,36 @@ +""" +This python file is adapted from https://github.com/lm-sys/FastChat/blob/main/fastchat/llm_judge/show_result.py + +FastChat (https://github.com/lm-sys/FastChat) is licensed under the Apache License, Version 2.0. + +Citation: +@misc{zheng2023judging, + title={Judging LLM-as-a-judge with MT-Bench and Chatbot Arena}, + author={Lianmin Zheng and Wei-Lin Chiang and Ying Sheng and Siyuan Zhuang and Zhanghao Wu + and Yonghao Zhuang and Zi Lin and Zhuohan Li and Dacheng Li and Eric. P Xing and Hao Zhang + and Joseph E. Gonzalez and Ion Stoica}, + year={2023}, + eprint={2306.05685}, + archivePrefix={arXiv}, + primaryClass={cs.CL} +} +""" + +import argparse + +from fastchat.llm_judge.show_result import display_result_single + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input-file", type=str, default=None) + parser.add_argument("--bench-name", type=str, default="mt_bench") + parser.add_argument("--judge-model", type=str, default="gpt-4") + parser.add_argument( + "--model-list", + type=str, + nargs="+", + default=None, + help="A list of models to be evaluated", + ) + args = parser.parse_args() + display_result_single(args) From dd5e317ac5070cd5a7f3118b3447e6dc96db808d Mon Sep 17 00:00:00 2001 From: William Lindskog <60471142+WilliamLindskog@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:07:37 -0400 Subject: [PATCH 166/188] docs(framework) Update Flower Architecture documentation (#4131) Co-authored-by: William --- doc/source/contributor-explanation-architecture.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/contributor-explanation-architecture.rst b/doc/source/contributor-explanation-architecture.rst index a20a84313118..48b43cf3f2b8 100644 --- a/doc/source/contributor-explanation-architecture.rst +++ b/doc/source/contributor-explanation-architecture.rst @@ -1,6 +1,8 @@ Flower Architecture =================== +This document provides an overview of the Flower architecture. The architecture is designed to be modular and flexible, and can use two different types of engines: Deployment Engine and Simulation Engine. + Edge Client Engine ------------------ From 486cac3c5e68f717ad023e87e01bbb87c115c37d Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 5 Sep 2024 11:39:28 +0200 Subject: [PATCH 167/188] ci(framework:skip) Update docker/build-push-action to 6.7.0 (#4135) Signed-off-by: Robert Steiner --- .github/workflows/_docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index ac88502b748a..1f36de550659 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -104,7 +104,7 @@ jobs: uses: Wandalen/wretry.action@6feedb7dedadeb826de0f45ff482b53b379a7844 # v3.5.0 id: build with: - action: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + action: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 attempt_limit: 60 # 60 attempts * (9 secs delay + 1 sec retry) = ~10 mins attempt_delay: 9000 # 9 secs with: | From 43174b7201c6ea9571c3a73b0d86ebc055c9e2a6 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 5 Sep 2024 13:27:52 +0200 Subject: [PATCH 168/188] ci(framework:skip) Replace QEMU with ARM runner (#4129) Signed-off-by: Robert Steiner --- .github/workflows/_docker-build.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/_docker-build.yml b/.github/workflows/_docker-build.yml index 1f36de550659..227b0d7482ae 100644 --- a/.github/workflows/_docker-build.yml +++ b/.github/workflows/_docker-build.yml @@ -36,7 +36,7 @@ permissions: jobs: build: name: Build image - runs-on: ubuntu-22.04 + runs-on: ${{ matrix.platform.runner-os }} timeout-minutes: 180 outputs: build-id: ${{ steps.build-id.outputs.id }} @@ -44,10 +44,8 @@ jobs: fail-fast: true matrix: platform: [ - # build-push action and qemu use different platform names - # therefore we create a map - { name: "amd64", qemu: "", docker: "linux/amd64" }, - { name: "arm64", qemu: "arm64", docker: "linux/arm64" }, + { name: "amd64", docker: "linux/amd64", runner-os: "ubuntu-22.04" }, + { name: "arm64", docker: "linux/arm64", runner-os: "ubuntu-4-core-arm64" }, ] steps: - name: Create build id @@ -79,12 +77,6 @@ jobs: print(build_args, file=fh) print("EOF", file=fh) - - name: Set up QEMU - if: matrix.platform.qemu != '' - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - with: - platforms: ${{ matrix.platform.qemu }} - - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 From 3457573d5ea9e9de3e2c5d826cc079d8d6a707fd Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Thu, 5 Sep 2024 12:44:11 +0100 Subject: [PATCH 169/188] refactor(examples) Update xgboost quickstart example (#4088) Co-authored-by: jafermarq Co-authored-by: Taner Topal --- examples/xgboost-quickstart/README.md | 82 +++---- examples/xgboost-quickstart/client.py | 207 ------------------ examples/xgboost-quickstart/pyproject.toml | 46 +++- examples/xgboost-quickstart/run.sh | 17 -- examples/xgboost-quickstart/server.py | 48 ---- .../xgboost_quickstart/__init__.py | 1 + .../xgboost_quickstart/client_app.py | 139 ++++++++++++ .../xgboost_quickstart/server_app.py | 54 +++++ .../xgboost_quickstart/task.py | 71 ++++++ 9 files changed, 337 insertions(+), 328 deletions(-) delete mode 100644 examples/xgboost-quickstart/client.py delete mode 100755 examples/xgboost-quickstart/run.sh delete mode 100644 examples/xgboost-quickstart/server.py create mode 100644 examples/xgboost-quickstart/xgboost_quickstart/__init__.py create mode 100644 examples/xgboost-quickstart/xgboost_quickstart/client_app.py create mode 100644 examples/xgboost-quickstart/xgboost_quickstart/server_app.py create mode 100644 examples/xgboost-quickstart/xgboost_quickstart/task.py diff --git a/examples/xgboost-quickstart/README.md b/examples/xgboost-quickstart/README.md index fa3e9d0dc6fb..a7b047c090f0 100644 --- a/examples/xgboost-quickstart/README.md +++ b/examples/xgboost-quickstart/README.md @@ -4,7 +4,7 @@ dataset: [HIGGS] framework: [xgboost] --- -# Flower Example using XGBoost +# Federated Learning with XGBoost and Flower (Quickstart Example) This example demonstrates how to perform EXtreme Gradient Boosting (XGBoost) within Flower using `xgboost` package. We use [HIGGS](https://archive.ics.uci.edu/dataset/280/higgs) dataset for this example to perform a binary classification task. @@ -12,72 +12,60 @@ Tree-based with bagging method is used for aggregation on the server. This project provides a minimal code example to enable you to get started quickly. For a more comprehensive code example, take a look at [xgboost-comprehensive](https://github.com/adap/flower/tree/main/examples/xgboost-comprehensive). -## Project Setup +## Set up the project -Start by cloning the example project. We prepared a single-line command that you can copy into your shell which will checkout the example for you: +### Clone the project -```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/xgboost-quickstart . && rm -rf flower && cd xgboost-quickstart -``` - -This will create a new directory called `xgboost-quickstart` containing the following files: - -``` --- README.md <- Your're reading this right now --- server.py <- Defines the server-side logic --- client.py <- Defines the client-side logic --- run.sh <- Commands to run experiments --- pyproject.toml <- Example dependencies -``` - -### Installing Dependencies - -Project dependencies (such as `xgboost` and `flwr`) are defined in `pyproject.toml`. You can install the dependencies by invoking `pip`: +Start by cloning the example project: ```shell -# From a new python environment, run: -pip install . +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/xgboost-quickstart . \ + && rm -rf _tmp \ + && cd xgboost-quickstart ``` -Then, to verify that everything works correctly you can run the following command: +This will create a new directory called `xgboost-quickstart` with the following structure: ```shell -python3 -c "import flwr" +xgboost-quickstart +β”œβ”€β”€ xgboost_quickstart +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your utilities and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -If you don't see any errors you're good to go! +### Install dependencies and project -## Run Federated Learning with XGBoost and Flower +Install the dependencies defined in `pyproject.toml` as well as the `xgboost_quickstart` package. -Afterwards you are ready to start the Flower server as well as the clients. -You can simply start the server in a terminal as follows: - -```shell -python3 server.py +```bash +pip install -e . ``` -Now you are ready to start the Flower clients which will participate in the learning. -To do so simply open two more terminal windows and run the following commands. +## Run the project -Start client 1 in the first terminal: +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. -```shell -python3 client.py --partition-id=0 +### Run with the Simulation Engine + +```bash +flwr run . ``` -Start client 2 in the second terminal: +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: -```shell -python3 client.py --partition-id=1 +```bash +flwr run . --run-config "num-server-rounds=5 params.eta=0.05" ``` -You will see that XGBoost is starting a federated training. - -Alternatively, you can use `run.sh` to run the same experiment in a single terminal as follows: +> \[!TIP\] +> For a more detailed walk-through check our [quickstart XGBoost tutorial](https://flower.ai/docs/framework/tutorial-quickstart-xgboost.html) -```shell -poetry run ./run.sh -``` +### Run with the Deployment Engine -Look at the [code](https://github.com/adap/flower/tree/main/examples/xgboost-quickstart) -and [tutorial](https://flower.ai/docs/framework/tutorial-quickstart-xgboost.html) for a detailed explanation. +> \[!NOTE\] +> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/xgboost-quickstart/client.py b/examples/xgboost-quickstart/client.py deleted file mode 100644 index d505a7ede785..000000000000 --- a/examples/xgboost-quickstart/client.py +++ /dev/null @@ -1,207 +0,0 @@ -import argparse -import warnings -from logging import INFO -from typing import Union - -import flwr as fl -import xgboost as xgb -from datasets import Dataset, DatasetDict -from flwr.common import ( - Code, - EvaluateIns, - EvaluateRes, - FitIns, - FitRes, - GetParametersIns, - GetParametersRes, - Parameters, - Status, -) -from flwr.common.logger import log -from flwr_datasets import FederatedDataset -from flwr_datasets.partitioner import IidPartitioner - -warnings.filterwarnings("ignore", category=UserWarning) - -# Define arguments parser for the client/partition ID. -parser = argparse.ArgumentParser() -parser.add_argument( - "--partition-id", - default=0, - type=int, - help="Partition ID used for the current client.", -) -args = parser.parse_args() - - -# Define data partitioning related functions -def train_test_split(partition: Dataset, test_fraction: float, seed: int): - """Split the data into train and validation set given split rate.""" - train_test = partition.train_test_split(test_size=test_fraction, seed=seed) - partition_train = train_test["train"] - partition_test = train_test["test"] - - num_train = len(partition_train) - num_test = len(partition_test) - - return partition_train, partition_test, num_train, num_test - - -def transform_dataset_to_dmatrix(data: Union[Dataset, DatasetDict]) -> xgb.core.DMatrix: - """Transform dataset to DMatrix format for xgboost.""" - x = data["inputs"] - y = data["label"] - new_data = xgb.DMatrix(x, label=y) - return new_data - - -# Load (HIGGS) dataset and conduct partitioning -# We use a small subset (num_partitions=30) of the dataset for demonstration to speed up the data loading process. -partitioner = IidPartitioner(num_partitions=30) -fds = FederatedDataset(dataset="jxie/higgs", partitioners={"train": partitioner}) - -# Load the partition for this `partition_id` -log(INFO, "Loading partition...") -partition = fds.load_partition(partition_id=args.partition_id, split="train") -partition.set_format("numpy") - -# Train/test splitting -train_data, valid_data, num_train, num_val = train_test_split( - partition, test_fraction=0.2, seed=42 -) - -# Reformat data to DMatrix for xgboost -log(INFO, "Reformatting data...") -train_dmatrix = transform_dataset_to_dmatrix(train_data) -valid_dmatrix = transform_dataset_to_dmatrix(valid_data) - -# Hyper-parameters for xgboost training -num_local_round = 1 -params = { - "objective": "binary:logistic", - "eta": 0.1, # Learning rate - "max_depth": 8, - "eval_metric": "auc", - "nthread": 16, - "num_parallel_tree": 1, - "subsample": 1, - "tree_method": "hist", -} - - -# Define Flower client -class XgbClient(fl.client.Client): - def __init__( - self, - train_dmatrix, - valid_dmatrix, - num_train, - num_val, - num_local_round, - params, - ): - self.train_dmatrix = train_dmatrix - self.valid_dmatrix = valid_dmatrix - self.num_train = num_train - self.num_val = num_val - self.num_local_round = num_local_round - self.params = params - - def get_parameters(self, ins: GetParametersIns) -> GetParametersRes: - _ = (self, ins) - return GetParametersRes( - status=Status( - code=Code.OK, - message="OK", - ), - parameters=Parameters(tensor_type="", tensors=[]), - ) - - def _local_boost(self, bst_input): - # Update trees based on local training data. - for i in range(self.num_local_round): - bst_input.update(self.train_dmatrix, bst_input.num_boosted_rounds()) - - # Bagging: extract the last N=num_local_round trees for sever aggregation - bst = bst_input[ - bst_input.num_boosted_rounds() - - self.num_local_round : bst_input.num_boosted_rounds() - ] - - return bst - - def fit(self, ins: FitIns) -> FitRes: - global_round = int(ins.config["global_round"]) - if global_round == 1: - # First round local training - bst = xgb.train( - self.params, - self.train_dmatrix, - num_boost_round=self.num_local_round, - evals=[(self.valid_dmatrix, "validate"), (self.train_dmatrix, "train")], - ) - else: - bst = xgb.Booster(params=self.params) - for item in ins.parameters.tensors: - global_model = bytearray(item) - - # Load global model into booster - bst.load_model(global_model) - - # Local training - bst = self._local_boost(bst) - - # Save model - local_model = bst.save_raw("json") - local_model_bytes = bytes(local_model) - - return FitRes( - status=Status( - code=Code.OK, - message="OK", - ), - parameters=Parameters(tensor_type="", tensors=[local_model_bytes]), - num_examples=self.num_train, - metrics={}, - ) - - def evaluate(self, ins: EvaluateIns) -> EvaluateRes: - # Load global model - bst = xgb.Booster(params=self.params) - for para in ins.parameters.tensors: - para_b = bytearray(para) - bst.load_model(para_b) - - # Run evaluation - eval_results = bst.eval_set( - evals=[(self.valid_dmatrix, "valid")], - iteration=bst.num_boosted_rounds() - 1, - ) - auc = round(float(eval_results.split("\t")[1].split(":")[1]), 4) - - global_round = ins.config["global_round"] - log(INFO, f"AUC = {auc} at round {global_round}") - - return EvaluateRes( - status=Status( - code=Code.OK, - message="OK", - ), - loss=0.0, - num_examples=self.num_val, - metrics={"AUC": auc}, - ) - - -# Start Flower client -fl.client.start_client( - server_address="127.0.0.1:8080", - client=XgbClient( - train_dmatrix, - valid_dmatrix, - num_train, - num_val, - num_local_round, - params, - ).to_client(), -) diff --git a/examples/xgboost-quickstart/pyproject.toml b/examples/xgboost-quickstart/pyproject.toml index f1e451fe779a..da3561bfded4 100644 --- a/examples/xgboost-quickstart/pyproject.toml +++ b/examples/xgboost-quickstart/pyproject.toml @@ -3,17 +3,45 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "quickstart-xgboost" -version = "0.1.0" -description = "XGBoost Federated Learning Quickstart with Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] +name = "xgboost_quickstart" +version = "1.0.0" +description = "Federated Learning with XGBoost and Flower (Quickstart Example)" +license = "Apache-2.0" dependencies = [ - "flwr>=1.8.0,<2.0", - "flwr-datasets>=0.1.0,<1.0.0", - "xgboost>=2.0.0,<3.0.0", + "flwr-nightly[simulation]==1.11.0.dev20240826", + "flwr-datasets>=0.3.0", + "xgboost>=2.0.0", ] [tool.hatch.build.targets.wheel] packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "xgboost_quickstart.server_app:app" +clientapp = "xgboost_quickstart.client_app:app" + +[tool.flwr.app.config] +# ServerApp +num-server-rounds = 3 +fraction-fit = 0.1 +fraction-evaluate = 0.1 + +# ClientApp +local-epochs = 1 +params.objective = "binary:logistic" +params.eta = 0.1 # Learning rate +params.max-depth = 8 +params.eval-metric = "auc" +params.nthread = 16 +params.num-parallel-tree = 1 +params.subsample = 1 +params.tree-method = "hist" + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 20 diff --git a/examples/xgboost-quickstart/run.sh b/examples/xgboost-quickstart/run.sh deleted file mode 100755 index b35af58222ab..000000000000 --- a/examples/xgboost-quickstart/run.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -echo "Starting server" -python server.py & -sleep 5 # Sleep for 5s to give the server enough time to start - -for i in `seq 0 1`; do - echo "Starting client $i" - python3 client.py --partition-id=$i & -done - -# Enable CTRL+C to stop all background processes -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM -# Wait for all background processes to complete -wait diff --git a/examples/xgboost-quickstart/server.py b/examples/xgboost-quickstart/server.py deleted file mode 100644 index 2246d32686a4..000000000000 --- a/examples/xgboost-quickstart/server.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import Dict - -import flwr as fl -from flwr.server.strategy import FedXgbBagging - -# FL experimental settings -pool_size = 2 -num_rounds = 5 -num_clients_per_round = 2 -num_evaluate_clients = 2 - - -def evaluate_metrics_aggregation(eval_metrics): - """Return an aggregated metric (AUC) for evaluation.""" - total_num = sum([num for num, _ in eval_metrics]) - auc_aggregated = ( - sum([metrics["AUC"] * num for num, metrics in eval_metrics]) / total_num - ) - metrics_aggregated = {"AUC": auc_aggregated} - return metrics_aggregated - - -def config_func(rnd: int) -> Dict[str, str]: - """Return a configuration with global epochs.""" - config = { - "global_round": str(rnd), - } - return config - - -# Define strategy -strategy = FedXgbBagging( - fraction_fit=(float(num_clients_per_round) / pool_size), - min_fit_clients=num_clients_per_round, - min_available_clients=pool_size, - min_evaluate_clients=num_evaluate_clients, - fraction_evaluate=1.0, - evaluate_metrics_aggregation_fn=evaluate_metrics_aggregation, - on_evaluate_config_fn=config_func, - on_fit_config_fn=config_func, -) - -# Start Flower server -fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=num_rounds), - strategy=strategy, -) diff --git a/examples/xgboost-quickstart/xgboost_quickstart/__init__.py b/examples/xgboost-quickstart/xgboost_quickstart/__init__.py new file mode 100644 index 000000000000..470360b377a6 --- /dev/null +++ b/examples/xgboost-quickstart/xgboost_quickstart/__init__.py @@ -0,0 +1 @@ +"""xgboost_quickstart: A Flower / XGBoost app.""" diff --git a/examples/xgboost-quickstart/xgboost_quickstart/client_app.py b/examples/xgboost-quickstart/xgboost_quickstart/client_app.py new file mode 100644 index 000000000000..3aa199a10274 --- /dev/null +++ b/examples/xgboost-quickstart/xgboost_quickstart/client_app.py @@ -0,0 +1,139 @@ +"""xgboost_quickstart: A Flower / XGBoost app.""" + +import warnings + +from flwr.common.context import Context + +import xgboost as xgb +from flwr.client import Client, ClientApp +from flwr.common.config import unflatten_dict +from flwr.common import ( + Code, + EvaluateIns, + EvaluateRes, + FitIns, + FitRes, + Parameters, + Status, +) + +from xgboost_quickstart.task import load_data, replace_keys + +warnings.filterwarnings("ignore", category=UserWarning) + + +# Define Flower Client and client_fn +class FlowerClient(Client): + def __init__( + self, + train_dmatrix, + valid_dmatrix, + num_train, + num_val, + num_local_round, + params, + ): + self.train_dmatrix = train_dmatrix + self.valid_dmatrix = valid_dmatrix + self.num_train = num_train + self.num_val = num_val + self.num_local_round = num_local_round + self.params = params + + def _local_boost(self, bst_input): + # Update trees based on local training data. + for i in range(self.num_local_round): + bst_input.update(self.train_dmatrix, bst_input.num_boosted_rounds()) + + # Bagging: extract the last N=num_local_round trees for sever aggregation + bst = bst_input[ + bst_input.num_boosted_rounds() + - self.num_local_round : bst_input.num_boosted_rounds() + ] + + return bst + + def fit(self, ins: FitIns) -> FitRes: + global_round = int(ins.config["global_round"]) + if global_round == 1: + # First round local training + bst = xgb.train( + self.params, + self.train_dmatrix, + num_boost_round=self.num_local_round, + evals=[(self.valid_dmatrix, "validate"), (self.train_dmatrix, "train")], + ) + else: + bst = xgb.Booster(params=self.params) + global_model = bytearray(ins.parameters.tensors[0]) + + # Load global model into booster + bst.load_model(global_model) + + # Local training + bst = self._local_boost(bst) + + # Save model + local_model = bst.save_raw("json") + local_model_bytes = bytes(local_model) + + return FitRes( + status=Status( + code=Code.OK, + message="OK", + ), + parameters=Parameters(tensor_type="", tensors=[local_model_bytes]), + num_examples=self.num_train, + metrics={}, + ) + + def evaluate(self, ins: EvaluateIns) -> EvaluateRes: + # Load global model + bst = xgb.Booster(params=self.params) + para_b = bytearray(ins.parameters.tensors[0]) + bst.load_model(para_b) + + # Run evaluation + eval_results = bst.eval_set( + evals=[(self.valid_dmatrix, "valid")], + iteration=bst.num_boosted_rounds() - 1, + ) + auc = round(float(eval_results.split("\t")[1].split(":")[1]), 4) + + return EvaluateRes( + status=Status( + code=Code.OK, + message="OK", + ), + loss=0.0, + num_examples=self.num_val, + metrics={"AUC": auc}, + ) + + +def client_fn(context: Context): + # Load model and data + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + train_dmatrix, valid_dmatrix, num_train, num_val = load_data( + partition_id, num_partitions + ) + + cfg = replace_keys(unflatten_dict(context.run_config)) + num_local_round = cfg["local_epochs"] + + # Return Client instance + return FlowerClient( + train_dmatrix, + valid_dmatrix, + num_train, + num_val, + num_local_round, + cfg["params"], + ) + + +# Flower ClientApp +app = ClientApp( + client_fn, +) diff --git a/examples/xgboost-quickstart/xgboost_quickstart/server_app.py b/examples/xgboost-quickstart/xgboost_quickstart/server_app.py new file mode 100644 index 000000000000..6b81c6caa785 --- /dev/null +++ b/examples/xgboost-quickstart/xgboost_quickstart/server_app.py @@ -0,0 +1,54 @@ +"""xgboost_quickstart: A Flower / XGBoost app.""" + +from typing import Dict + +from flwr.common import Context, Parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig +from flwr.server.strategy import FedXgbBagging + + +def evaluate_metrics_aggregation(eval_metrics): + """Return an aggregated metric (AUC) for evaluation.""" + total_num = sum([num for num, _ in eval_metrics]) + auc_aggregated = ( + sum([metrics["AUC"] * num for num, metrics in eval_metrics]) / total_num + ) + metrics_aggregated = {"AUC": auc_aggregated} + return metrics_aggregated + + +def config_func(rnd: int) -> Dict[str, str]: + """Return a configuration with global epochs.""" + config = { + "global_round": str(rnd), + } + return config + + +def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + fraction_evaluate = context.run_config["fraction-evaluate"] + + # Init an empty Parameter + parameters = Parameters(tensor_type="", tensors=[]) + + # Define strategy + strategy = FedXgbBagging( + fraction_fit=fraction_fit, + fraction_evaluate=fraction_evaluate, + evaluate_metrics_aggregation_fn=evaluate_metrics_aggregation, + on_evaluate_config_fn=config_func, + on_fit_config_fn=config_func, + initial_parameters=parameters, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp( + server_fn=server_fn, +) diff --git a/examples/xgboost-quickstart/xgboost_quickstart/task.py b/examples/xgboost-quickstart/xgboost_quickstart/task.py new file mode 100644 index 000000000000..09916d9ac04a --- /dev/null +++ b/examples/xgboost-quickstart/xgboost_quickstart/task.py @@ -0,0 +1,71 @@ +"""xgboost_quickstart: A Flower / XGBoost app.""" + +from logging import INFO + +import xgboost as xgb +from flwr.common import log +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + + +def train_test_split(partition, test_fraction, seed): + """Split the data into train and validation set given split rate.""" + train_test = partition.train_test_split(test_size=test_fraction, seed=seed) + partition_train = train_test["train"] + partition_test = train_test["test"] + + num_train = len(partition_train) + num_test = len(partition_test) + + return partition_train, partition_test, num_train, num_test + + +def transform_dataset_to_dmatrix(data): + """Transform dataset to DMatrix format for xgboost.""" + x = data["inputs"] + y = data["label"] + new_data = xgb.DMatrix(x, label=y) + return new_data + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id, num_clients): + """Load partition HIGGS data.""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_clients) + fds = FederatedDataset( + dataset="jxie/higgs", + partitioners={"train": partitioner}, + ) + + # Load the partition for this `partition_id` + partition = fds.load_partition(partition_id, split="train") + partition.set_format("numpy") + + # Train/test splitting + train_data, valid_data, num_train, num_val = train_test_split( + partition, test_fraction=0.2, seed=42 + ) + + # Reformat data to DMatrix for xgboost + log(INFO, "Reformatting data...") + train_dmatrix = transform_dataset_to_dmatrix(train_data) + valid_dmatrix = transform_dataset_to_dmatrix(valid_data) + + return train_dmatrix, valid_dmatrix, num_train, num_val + + +def replace_keys(input_dict, match="-", target="_"): + """Recursively replace match string with target string in dictionary keys.""" + new_dict = {} + for key, value in input_dict.items(): + new_key = key.replace(match, target) + if isinstance(value, dict): + new_dict[new_key] = replace_keys(value, match, target) + else: + new_dict[new_key] = value + return new_dict From 1187c707f1894924bfa693d99611cf6f93431835 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 5 Sep 2024 17:01:54 +0200 Subject: [PATCH 170/188] fix(framework:skip) Determine flwr version by tag instead of poetry (#4141) Signed-off-by: Robert Steiner --- .github/workflows/framework-release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/framework-release.yml b/.github/workflows/framework-release.yml index 812d5b1e398e..e608329872de 100644 --- a/.github/workflows/framework-release.yml +++ b/.github/workflows/framework-release.yml @@ -16,6 +16,8 @@ jobs: if: ${{ github.repository == 'adap/flower' }} name: Publish release runs-on: ubuntu-22.04 + outputs: + flwr-version: ${{ steps.publish.outputs.flwr-version }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -26,10 +28,12 @@ jobs: uses: ./.github/actions/bootstrap - name: Get artifacts and publish + id: publish env: GITHUB_REF: ${{ github.ref }} run: | TAG_NAME=$(echo "${GITHUB_REF_NAME}" | cut -c2-) + echo "flwr-version=$TAG_NAME" >> "$GITHUB_OUTPUT" wheel_name="flwr-${TAG_NAME}-py3-none-any.whl" tar_name="flwr-${TAG_NAME}.tar.gz" @@ -67,8 +71,7 @@ jobs: - id: matrix run: | - FLWR_VERSION=$(poetry version -s) - python dev/build-docker-image-matrix.py --flwr-version "${FLWR_VERSION}" > matrix.json + python dev/build-docker-image-matrix.py --flwr-version "${{ needs.publish.outputs.flwr-version }}" > matrix.json echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT build-base-images: From f290fe2d49e12a871c33c0ce465ea1d021a4e863 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 5 Sep 2024 22:31:08 +0100 Subject: [PATCH 171/188] refactor(examples) Update fl-dp-sa example (#4137) Co-authored-by: jafermarq --- examples/fl-dp-sa/README.md | 61 +++++++++++++++---- examples/fl-dp-sa/fl_dp_sa/__init__.py | 2 +- examples/fl-dp-sa/fl_dp_sa/client.py | 42 ------------- examples/fl-dp-sa/fl_dp_sa/client_app.py | 50 +++++++++++++++ .../fl_dp_sa/{server.py => server_app.py} | 59 ++++++++++-------- examples/fl-dp-sa/fl_dp_sa/task.py | 39 ++++++------ examples/fl-dp-sa/flower.toml | 13 ---- examples/fl-dp-sa/pyproject.toml | 49 ++++++++++----- examples/fl-dp-sa/requirements.txt | 4 -- 9 files changed, 188 insertions(+), 131 deletions(-) delete mode 100644 examples/fl-dp-sa/fl_dp_sa/client.py create mode 100644 examples/fl-dp-sa/fl_dp_sa/client_app.py rename examples/fl-dp-sa/fl_dp_sa/{server.py => server_app.py} (56%) delete mode 100644 examples/fl-dp-sa/flower.toml delete mode 100644 examples/fl-dp-sa/requirements.txt diff --git a/examples/fl-dp-sa/README.md b/examples/fl-dp-sa/README.md index 65c8a5b18fa8..61a6c80f3556 100644 --- a/examples/fl-dp-sa/README.md +++ b/examples/fl-dp-sa/README.md @@ -1,28 +1,63 @@ --- -tags: [basic, vision, fds] +tags: [DP, SecAgg, vision, fds] dataset: [MNIST] framework: [torch, torchvision] --- -# Example of Flower App with DP and SA +# Flower Example on MNIST with Differential Privacy and Secure Aggregation -This is a simple example that utilizes central differential privacy with client-side fixed clipping and secure aggregation. -Note: This example is designed for a small number of rounds and is intended for demonstration purposes. +This example demonstrates a federated learning setup using the Flower, incorporating central differential privacy (DP) with client-side fixed clipping and secure aggregation (SA). It is intended for a small number of rounds for demonstration purposes. -## Install dependencies +This example is similar to the [quickstart-pytorch example](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) and extends it by integrating central differential privacy and secure aggregation. For more details on differential privacy and secure aggregation in Flower, please refer to the documentation [here](https://flower.ai/docs/framework/how-to-use-differential-privacy.html) and [here](https://flower.ai/docs/framework/contributor-ref-secure-aggregation-protocols.html). -```bash -# Using pip -pip install . +## Set up the project + +### Clone the project + +Start by cloning the example project: + +```shell +git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/fl-dp-sa . && rm -rf flower && cd fl-dp-sa +``` + +This will create a new directory called `fl-dp-sa` containing the following files: -# Or using Poetry -poetry install +```shell +fl-dp-sa +β”œβ”€β”€ fl_dp_sa +β”‚ β”œβ”€β”€ client_app.py # Defines your ClientApp +β”‚ β”œβ”€β”€ server_app.py # Defines your ServerApp +β”‚ └── task.py # Defines your model, training, and data loading +β”œβ”€β”€ pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -## Run +### Install dependencies and project -The example uses the MNIST dataset with a total of 100 clients, with 20 clients sampled in each round. The hyperparameters for DP and SecAgg are specified in `server.py`. +Install the dependencies defined in `pyproject.toml` as well as the `fl_dp_sa` package. ```shell -flower-simulation --server-app fl_dp_sa.server:app --client-app fl_dp_sa.client:app --num-supernodes 100 +# From a new python environment, run: +pip install -e . +``` + +## Run the project + +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. + +### Run with the Simulation Engine + +```bash +flwr run . +``` + +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example: + +```bash +flwr run . --run-config "noise-multiplier=0.1 clipping-norm=5" ``` + +### Run with the Deployment Engine + +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. diff --git a/examples/fl-dp-sa/fl_dp_sa/__init__.py b/examples/fl-dp-sa/fl_dp_sa/__init__.py index 741260348ab8..c5c9a7e9581c 100644 --- a/examples/fl-dp-sa/fl_dp_sa/__init__.py +++ b/examples/fl-dp-sa/fl_dp_sa/__init__.py @@ -1 +1 @@ -"""fl_dp_sa: A Flower / PyTorch app.""" +"""fl_dp_sa: Flower Example using Differential Privacy and Secure Aggregation.""" diff --git a/examples/fl-dp-sa/fl_dp_sa/client.py b/examples/fl-dp-sa/fl_dp_sa/client.py deleted file mode 100644 index b3b02c6e9d61..000000000000 --- a/examples/fl-dp-sa/fl_dp_sa/client.py +++ /dev/null @@ -1,42 +0,0 @@ -"""fl_dp_sa: A Flower / PyTorch app.""" - -from flwr.client import ClientApp, NumPyClient -from flwr.client.mod import fixedclipping_mod, secaggplus_mod - -from fl_dp_sa.task import DEVICE, Net, get_weights, load_data, set_weights, test, train - -# Load model and data (simple CNN, CIFAR-10) -net = Net().to(DEVICE) - - -# Define FlowerClient and client_fn -class FlowerClient(NumPyClient): - def __init__(self, trainloader, testloader) -> None: - self.trainloader = trainloader - self.testloader = testloader - - def fit(self, parameters, config): - set_weights(net, parameters) - results = train(net, self.trainloader, self.testloader, epochs=1, device=DEVICE) - return get_weights(net), len(self.trainloader.dataset), results - - def evaluate(self, parameters, config): - set_weights(net, parameters) - loss, accuracy = test(net, self.testloader) - return loss, len(self.testloader.dataset), {"accuracy": accuracy} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - trainloader, testloader = load_data(partition_id=int(cid)) - return FlowerClient(trainloader, testloader).to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, - mods=[ - secaggplus_mod, - fixedclipping_mod, - ], -) diff --git a/examples/fl-dp-sa/fl_dp_sa/client_app.py b/examples/fl-dp-sa/fl_dp_sa/client_app.py new file mode 100644 index 000000000000..5630d4f4d14f --- /dev/null +++ b/examples/fl-dp-sa/fl_dp_sa/client_app.py @@ -0,0 +1,50 @@ +"""fl_dp_sa: Flower Example using Differential Privacy and Secure Aggregation.""" + +import torch +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context +from flwr.client.mod import fixedclipping_mod, secaggplus_mod + +from fl_dp_sa.task import Net, get_weights, load_data, set_weights, test, train + + +class FlowerClient(NumPyClient): + def __init__(self, trainloader, testloader) -> None: + self.net = Net() + self.trainloader = trainloader + self.testloader = testloader + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + def fit(self, parameters, config): + set_weights(self.net, parameters) + results = train( + self.net, + self.trainloader, + self.testloader, + epochs=1, + device=self.device, + ) + return get_weights(self.net), len(self.trainloader.dataset), results + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.testloader, self.device) + return loss, len(self.testloader.dataset), {"accuracy": accuracy} + + +def client_fn(context: Context): + partition_id = context.node_config["partition-id"] + trainloader, testloader = load_data( + partition_id=partition_id, num_partitions=context.node_config["num-partitions"] + ) + return FlowerClient(trainloader, testloader).to_client() + + +# Flower ClientApp +app = ClientApp( + client_fn=client_fn, + mods=[ + secaggplus_mod, + fixedclipping_mod, + ], +) diff --git a/examples/fl-dp-sa/fl_dp_sa/server.py b/examples/fl-dp-sa/fl_dp_sa/server_app.py similarity index 56% rename from examples/fl-dp-sa/fl_dp_sa/server.py rename to examples/fl-dp-sa/fl_dp_sa/server_app.py index 3ec0ba757b0d..1704b4942ff8 100644 --- a/examples/fl-dp-sa/fl_dp_sa/server.py +++ b/examples/fl-dp-sa/fl_dp_sa/server_app.py @@ -1,20 +1,22 @@ -"""fl_dp_sa: A Flower / PyTorch app.""" +"""fl_dp_sa: Flower Example using Differential Privacy and Secure Aggregation.""" from typing import List, Tuple from flwr.common import Context, Metrics, ndarrays_to_parameters -from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig +from flwr.server import ( + Driver, + LegacyContext, + ServerApp, + ServerConfig, +) from flwr.server.strategy import DifferentialPrivacyClientSideFixedClipping, FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow from fl_dp_sa.task import Net, get_weights -# Define metric aggregation function def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: examples = [num_examples for num_examples, _ in metrics] - - # Multiply accuracy of each client by number of examples used train_losses = [num_examples * m["train_loss"] for num_examples, m in metrics] train_accuracies = [ num_examples * m["train_accuracy"] for num_examples, m in metrics @@ -22,7 +24,6 @@ def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: val_losses = [num_examples * m["val_loss"] for num_examples, m in metrics] val_accuracies = [num_examples * m["val_accuracy"] for num_examples, m in metrics] - # Aggregate and return custom metric (weighted average) return { "train_loss": sum(train_losses) / sum(examples), "train_accuracy": sum(train_accuracies) / sum(examples), @@ -31,30 +32,36 @@ def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: } -# Initialize model parameters -ndarrays = get_weights(Net()) -parameters = ndarrays_to_parameters(ndarrays) +app = ServerApp() -# Define strategy -strategy = FedAvg( - fraction_fit=0.2, - fraction_evaluate=0.0, # Disable evaluation for demo purpose - min_fit_clients=20, - min_available_clients=20, - fit_metrics_aggregation_fn=weighted_average, - initial_parameters=parameters, -) -strategy = DifferentialPrivacyClientSideFixedClipping( - strategy, noise_multiplier=0.2, clipping_norm=10, num_sampled_clients=20 -) +@app.main() +def main(driver: Driver, context: Context) -> None: + # Initialize global model + model_weights = get_weights(Net()) + parameters = ndarrays_to_parameters(model_weights) + + # Note: The fraction_fit value is configured based on the DP hyperparameter `num-sampled-clients`. + strategy = FedAvg( + fraction_fit=0.2, + fraction_evaluate=0.0, + min_fit_clients=20, + fit_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) -app = ServerApp() + noise_multiplier = context.run_config["noise-multiplier"] + clipping_norm = context.run_config["clipping-norm"] + num_sampled_clients = context.run_config["num-sampled-clients"] + strategy = DifferentialPrivacyClientSideFixedClipping( + strategy, + noise_multiplier=noise_multiplier, + clipping_norm=clipping_norm, + num_sampled_clients=num_sampled_clients, + ) -@app.main() -def main(driver: Driver, context: Context) -> None: # Construct the LegacyContext context = LegacyContext( context=context, @@ -65,8 +72,8 @@ def main(driver: Driver, context: Context) -> None: # Create the train/evaluate workflow workflow = DefaultWorkflow( fit_workflow=SecAggPlusWorkflow( - num_shares=7, - reconstruction_threshold=4, + num_shares=context.run_config["num-shares"], + reconstruction_threshold=context.run_config["reconstruction-threshold"], ) ) diff --git a/examples/fl-dp-sa/fl_dp_sa/task.py b/examples/fl-dp-sa/fl_dp_sa/task.py index 5b4fd7dee592..c145cebe1378 100644 --- a/examples/fl-dp-sa/fl_dp_sa/task.py +++ b/examples/fl-dp-sa/fl_dp_sa/task.py @@ -1,24 +1,22 @@ -"""fl_dp_sa: A Flower / PyTorch app.""" +"""fl_dp_sa: Flower Example using Differential Privacy and Secure Aggregation.""" from collections import OrderedDict -from logging import INFO import torch import torch.nn as nn import torch.nn.functional as F -from flwr.common.logger import log from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner from torch.utils.data import DataLoader from torchvision.transforms import Compose, Normalize, ToTensor -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") +fds = None # Cache FederatedDataset -class Net(nn.Module): - """Model.""" +class Net(nn.Module): def __init__(self) -> None: - super(Net, self).__init__() + super().__init__() self.conv1 = nn.Conv2d(1, 6, 3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 16, 5) @@ -36,9 +34,16 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return self.fc3(x) -def load_data(partition_id): +def load_data(partition_id: int, num_partitions: int): """Load partition MNIST data.""" - fds = FederatedDataset(dataset="mnist", partitioners={"train": 100}) + + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="ylecun/mnist", + partitioners={"train": partitioner}, + ) partition = fds.load_partition(partition_id) # Divide data on each node: 80% train, 20% test partition_train_test = partition.train_test_split(test_size=0.2, seed=42) @@ -70,8 +75,8 @@ def train(net, trainloader, valloader, epochs, device): loss.backward() optimizer.step() - train_loss, train_acc = test(net, trainloader) - val_loss, val_acc = test(net, valloader) + train_loss, train_acc = test(net, trainloader, device) + val_loss, val_acc = test(net, valloader, device) results = { "train_loss": train_loss, @@ -82,17 +87,17 @@ def train(net, trainloader, valloader, epochs, device): return results -def test(net, testloader): +def test(net, testloader, device): """Validate the model on the test set.""" - net.to(DEVICE) + net.to(device) criterion = torch.nn.CrossEntropyLoss() correct, loss = 0, 0.0 with torch.no_grad(): for batch in testloader: - images = batch["image"].to(DEVICE) - labels = batch["label"].to(DEVICE) - outputs = net(images.to(DEVICE)) - labels = labels.to(DEVICE) + images = batch["image"].to(device) + labels = batch["label"].to(device) + outputs = net(images.to(device)) + labels = labels.to(device) loss += criterion(outputs, labels).item() correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() accuracy = correct / len(testloader.dataset) diff --git a/examples/fl-dp-sa/flower.toml b/examples/fl-dp-sa/flower.toml deleted file mode 100644 index ea2e98206791..000000000000 --- a/examples/fl-dp-sa/flower.toml +++ /dev/null @@ -1,13 +0,0 @@ -[project] -name = "fl_dp_sa" -version = "1.0.0" -description = "" -license = "Apache-2.0" -authors = [ - "The Flower Authors ", -] -readme = "README.md" - -[flower.components] -serverapp = "fl_dp_sa.server:app" -clientapp = "fl_dp_sa.client:app" diff --git a/examples/fl-dp-sa/pyproject.toml b/examples/fl-dp-sa/pyproject.toml index 1ca343b072d9..fbb463cc1c05 100644 --- a/examples/fl-dp-sa/pyproject.toml +++ b/examples/fl-dp-sa/pyproject.toml @@ -1,21 +1,40 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] +[project] name = "fl-dp-sa" -version = "0.1.0" -description = "" +version = "1.0.0" +description = "Central Differential Privacy and Secure Aggregation in Flower" license = "Apache-2.0" -authors = [ - "The Flower Authors ", +dependencies = [ + "flwr[simulation]>=1.11.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", ] -readme = "README.md" -[tool.poetry.dependencies] -python = "^3.9" -# Mandatory dependencies -flwr = { version = "^1.8.0", extras = ["simulation"] } -flwr-datasets = { version = "0.0.2", extras = ["vision"] } -torch = "2.2.1" -torchvision = "0.17.1" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "fl_dp_sa.server_app:app" +clientapp = "fl_dp_sa.client_app:app" + +[tool.flwr.app.config] +# Parameters for the DP +noise-multiplier = 0.2 +clipping-norm = 10 +num-sampled-clients = 20 +# Parameters for the SecAgg+ protocol +num-shares = 7 +reconstruction-threshold = 4 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 100 \ No newline at end of file diff --git a/examples/fl-dp-sa/requirements.txt b/examples/fl-dp-sa/requirements.txt deleted file mode 100644 index f20b9d71e339..000000000000 --- a/examples/fl-dp-sa/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flwr[simulation]>=1.8.0 -flwr-datasets[vision]==0.0.2 -torch==2.2.1 -torchvision==0.17.1 From 4bdcf612daa8a86092a7a3f4a683694b6347924f Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 6 Sep 2024 11:33:12 +0200 Subject: [PATCH 172/188] feat(framework:skip) Allow to flwr via a direct reference (#4144) Signed-off-by: Robert Steiner Co-authored-by: Taner Topal --- src/docker/base/alpine/Dockerfile | 17 +++++++++++++---- src/docker/base/ubuntu/Dockerfile | 15 +++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/docker/base/alpine/Dockerfile b/src/docker/base/alpine/Dockerfile index 3e6a246e53c1..ee1e11b2d070 100644 --- a/src/docker/base/alpine/Dockerfile +++ b/src/docker/base/alpine/Dockerfile @@ -33,6 +33,8 @@ RUN apk add --no-cache \ # require for compiling grpcio on ARM64 g++ \ libffi-dev \ + # required for installing flwr via git + git \ # create virtual env && python -m venv /python/venv @@ -42,12 +44,19 @@ ENV PATH=/python/venv/bin:$PATH # Install specific version of pip, setuptools and flwr ARG PIP_VERSION ARG SETUPTOOLS_VERSION -ARG FLWR_VERSION -ARG FLWR_PACKAGE=flwr RUN pip install -U --no-cache-dir \ pip==${PIP_VERSION} \ - setuptools==${SETUPTOOLS_VERSION} \ - ${FLWR_PACKAGE}==${FLWR_VERSION} + setuptools==${SETUPTOOLS_VERSION} + +ARG FLWR_VERSION +ARG FLWR_VERSION_REF +ARG FLWR_PACKAGE=flwr +# hadolint ignore=DL3013 +RUN if [ -z "${FLWR_VERSION_REF}" ]; then \ + pip install -U --no-cache-dir ${FLWR_PACKAGE}==${FLWR_VERSION}; \ + else \ + pip install -U --no-cache-dir ${FLWR_PACKAGE}@${FLWR_VERSION_REF}; \ + fi FROM python:${PYTHON_VERSION}-${DISTRO}${DISTRO_VERSION} AS base diff --git a/src/docker/base/ubuntu/Dockerfile b/src/docker/base/ubuntu/Dockerfile index ddc662a0ae98..47655b1a52a1 100644 --- a/src/docker/base/ubuntu/Dockerfile +++ b/src/docker/base/ubuntu/Dockerfile @@ -60,12 +60,19 @@ RUN pip install -U --no-cache-dir pip==${PIP_VERSION} setuptools==${SETUPTOOLS_V && python -m venv /python/venv ENV PATH=/python/venv/bin:$PATH -ARG FLWR_VERSION -ARG FLWR_PACKAGE=flwr RUN pip install -U --no-cache-dir \ pip==${PIP_VERSION} \ - setuptools==${SETUPTOOLS_VERSION} \ - ${FLWR_PACKAGE}==${FLWR_VERSION} + setuptools==${SETUPTOOLS_VERSION} + +ARG FLWR_VERSION +ARG FLWR_VERSION_REF +ARG FLWR_PACKAGE=flwr +# hadolint ignore=DL3013 +RUN if [ -z "${FLWR_VERSION_REF}" ]; then \ + pip install -U --no-cache-dir ${FLWR_PACKAGE}==${FLWR_VERSION}; \ + else \ + pip install -U --no-cache-dir ${FLWR_PACKAGE}@${FLWR_VERSION_REF}; \ + fi FROM $DISTRO:$DISTRO_VERSION AS base From a0b4b06c6aabe05910588c985e72c2bada3fa2d0 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 6 Sep 2024 12:28:49 +0200 Subject: [PATCH 173/188] feat(framework) Add Docker Compose distributed files (#3924) Signed-off-by: Robert Steiner --- src/docker/distributed/.gitignore | 3 + src/docker/distributed/certs.yml | 6 + src/docker/distributed/client/compose.yml | 128 ++++++++++++++++++++++ src/docker/distributed/server/compose.yml | 67 +++++++++++ 4 files changed, 204 insertions(+) create mode 100644 src/docker/distributed/.gitignore create mode 100644 src/docker/distributed/certs.yml create mode 100644 src/docker/distributed/client/compose.yml create mode 100644 src/docker/distributed/server/compose.yml diff --git a/src/docker/distributed/.gitignore b/src/docker/distributed/.gitignore new file mode 100644 index 000000000000..1a11330c6e95 --- /dev/null +++ b/src/docker/distributed/.gitignore @@ -0,0 +1,3 @@ +superexec-certificates +superlink-certificates +server/state diff --git a/src/docker/distributed/certs.yml b/src/docker/distributed/certs.yml new file mode 100644 index 000000000000..48e157582e40 --- /dev/null +++ b/src/docker/distributed/certs.yml @@ -0,0 +1,6 @@ +services: + gen-certs: + build: + args: + SUPERLINK_IP: ${SUPERLINK_IP:-127.0.0.1} + SUPEREXEC_IP: ${SUPEREXEC_IP:-127.0.0.1} diff --git a/src/docker/distributed/client/compose.yml b/src/docker/distributed/client/compose.yml new file mode 100644 index 000000000000..ef69e40cc425 --- /dev/null +++ b/src/docker/distributed/client/compose.yml @@ -0,0 +1,128 @@ +services: + supernode-1: + image: flwr/supernode:${FLWR_VERSION:-1.11.0} + command: + - --superlink + - ${SUPERLINK_IP:-127.0.0.1}:9092 + - --supernode-address + - 0.0.0.0:9094 + - --isolation + - process + - --node-config + - "partition-id=0 num-partitions=2" + - --root-certificates + - certificates/ca.crt + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + + supernode-2: + image: flwr/supernode:${FLWR_VERSION:-1.11.0} + command: + - --superlink + - ${SUPERLINK_IP:-127.0.0.1}:9092 + - --supernode-address + - 0.0.0.0:9095 + - --isolation + - process + - --node-config + - "partition-id=1 num-partitions=2" + - --root-certificates + - certificates/ca.crt + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + + # uncomment to add another SuperNode + # + # supernode-3: + # image: flwr/supernode:${FLWR_VERSION:-1.11.0} + # command: + # - --superlink + # - ${SUPERLINK_IP:-127.0.0.1}:9092 + # - --supernode-address + # - 0.0.0.0:9096 + # - --isolation + # - process + # - --node-config + # - "partition-id=1 num-partitions=2" + # - --root-certificates + # - certificates/ca.crt + # secrets: + # - source: superlink-ca-certfile + # target: /app/certificates/ca.crt + + clientapp-1: + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flwr-clientapp"] + command: + - --supernode + - supernode-1:9094 + deploy: + resources: + limits: + cpus: "2" + stop_signal: SIGINT + depends_on: + - supernode-1 + + clientapp-2: + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flwr-clientapp"] + command: + - --supernode + - supernode-2:9095 + deploy: + resources: + limits: + cpus: "2" + stop_signal: SIGINT + depends_on: + - supernode-2 + + # uncomment to add another ClientApp + # + # clientapp-3: + # build: + # context: ${PROJECT_DIR:-.} + # dockerfile_inline: | + # FROM flwr/clientapp:${FLWR_VERSION:-1.11.0} + + # WORKDIR /app + # COPY --chown=app:app pyproject.toml . + # RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + # && python -m pip install -U --no-cache-dir . + + # ENTRYPOINT ["flwr-clientapp"] + # command: + # - --supernode + # - supernode-3:9096 + # deploy: + # resources: + # limits: + # cpus: "2" + # stop_signal: SIGINT + # depends_on: + # - supernode-3 + +secrets: + superlink-ca-certfile: + file: ../superlink-certificates/ca.crt diff --git a/src/docker/distributed/server/compose.yml b/src/docker/distributed/server/compose.yml new file mode 100644 index 000000000000..fc6dd6f58717 --- /dev/null +++ b/src/docker/distributed/server/compose.yml @@ -0,0 +1,67 @@ +services: + superlink: + image: flwr/superlink:${FLWR_VERSION:-1.11.0} + command: + - --ssl-ca-certfile=certificates/ca.crt + - --ssl-certfile=certificates/server.pem + - --ssl-keyfile=certificates/server.key + - --database=state/state.db + volumes: + - ./state/:/app/state/:rw + secrets: + - source: superlink-ca-certfile + target: /app/certificates/ca.crt + - source: superlink-certfile + target: /app/certificates/server.pem + - source: superlink-keyfile + target: /app/certificates/server.key + ports: + - 9092:9092 + + superexec: + build: + context: ${PROJECT_DIR:-.} + dockerfile_inline: | + FROM flwr/superexec:${FLWR_VERSION:-1.11.0} + + WORKDIR /app + COPY --chown=app:app pyproject.toml . + RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ + && python -m pip install -U --no-cache-dir . + + ENTRYPOINT ["flower-superexec"] + command: + - --executor + - flwr.superexec.deployment:executor + - --executor-config + - superlink="superlink:9091" root-certificates="certificates/superlink-ca.crt" + - --ssl-ca-certfile=certificates/ca.crt + - --ssl-certfile=certificates/server.pem + - --ssl-keyfile=certificates/server.key + secrets: + - source: superlink-ca-certfile + target: /app/certificates/superlink-ca.crt + - source: superexec-ca-certfile + target: /app/certificates/ca.crt + - source: superexec-certfile + target: /app/certificates/server.pem + - source: superexec-keyfile + target: /app/certificates/server.key + ports: + - 9093:9093 + depends_on: + - superlink + +secrets: + superlink-ca-certfile: + file: ../superlink-certificates/ca.crt + superlink-certfile: + file: ../superlink-certificates/server.pem + superlink-keyfile: + file: ../superlink-certificates/server.key + superexec-ca-certfile: + file: ../superexec-certificates/ca.crt + superexec-certfile: + file: ../superexec-certificates/server.pem + superexec-keyfile: + file: ../superexec-certificates/server.key From 9f51f3e983af448ab3ec0660cc851a4d070c6b53 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Fri, 6 Sep 2024 12:38:08 +0200 Subject: [PATCH 174/188] ci(*:skip) New Docker codeowners (#4149) Signed-off-by: Robert Steiner --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ccf031344f67..3e314c8d1de5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -35,3 +35,4 @@ README.md @jafermarq @tanertopal @danieljanes /.devcontainer @Robert-Steiner @Moep90 **/Dockerfile @Robert-Steiner @Moep90 **/*.Dockerfile @Robert-Steiner @Moep90 +src/docker @Robert-Steiner @Moep90 From f7b01dda6635a4e23d4a076353af2499f86508fd Mon Sep 17 00:00:00 2001 From: Javier Date: Fri, 6 Sep 2024 14:59:51 +0200 Subject: [PATCH 175/188] docs(framework:skip) Fix passing multiple arguments to --run-config in docs (#4150) --- doc/source/tutorial-quickstart-mlx.rst | 2 +- doc/source/tutorial-quickstart-pytorch.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial-quickstart-mlx.rst b/doc/source/tutorial-quickstart-mlx.rst index 0999bf44d3b7..675a08502d26 100644 --- a/doc/source/tutorial-quickstart-mlx.rst +++ b/doc/source/tutorial-quickstart-mlx.rst @@ -109,7 +109,7 @@ You can also override the parameters defined in .. code:: shell # Override some arguments - $ flwr run . --run-config num-server-rounds=5,lr=0.05 + $ flwr run . --run-config "num-server-rounds=5 lr=0.05" What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the ``ClientApp`` and diff --git a/doc/source/tutorial-quickstart-pytorch.rst b/doc/source/tutorial-quickstart-pytorch.rst index 4515e8d0eeb5..d00b9efbe16b 100644 --- a/doc/source/tutorial-quickstart-pytorch.rst +++ b/doc/source/tutorial-quickstart-pytorch.rst @@ -108,7 +108,7 @@ You can also override the parameters defined in the .. code:: shell # Override some arguments - $ flwr run . --run-config num-server-rounds=5,local-epochs=3 + $ flwr run . --run-config "num-server-rounds=5 local-epochs=3" What follows is an explanation of each component in the project you just created: dataset partition, the model, defining the ``ClientApp`` and From ceb39033d882353bc1bef014a79a3502db198c9b Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Fri, 6 Sep 2024 14:06:54 +0100 Subject: [PATCH 176/188] ci(*:skip) Refactor client-auth e2e CI (#4148) Co-authored-by: Daniel Nata Nugraha --- .github/workflows/e2e.yml | 2 +- e2e/test_superlink.sh | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 49e5b7bf1b36..53015310dcc1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -172,7 +172,7 @@ jobs: run: ./../test_superlink.sh bare sqlite - name: Run driver test with client authentication if: ${{ matrix.directory == 'e2e-bare-auth' }} - run: ./../test_superlink.sh bare client-auth + run: ./../test_superlink.sh "${{ matrix.directory }}" client-auth - name: Run reconnection test with SQLite database if: ${{ matrix.directory == 'e2e-bare' }} run: ./../test_reconnection.sh sqlite diff --git a/e2e/test_superlink.sh b/e2e/test_superlink.sh index 684f386bd388..2016f6da1933 100755 --- a/e2e/test_superlink.sh +++ b/e2e/test_superlink.sh @@ -2,7 +2,7 @@ set -e case "$1" in - e2e-bare-https) + e2e-bare-https | e2e-bare-auth) ./generate.sh server_arg="--ssl-ca-certfile certificates/ca.crt --ssl-certfile certificates/server.pem --ssl-keyfile certificates/server.key" client_arg="--root-certificates certificates/ca.crt" @@ -37,14 +37,11 @@ case "$2" in client_auth_2="" ;; client-auth) - ./generate.sh rest_arg_superlink="" rest_arg_supernode="" server_address="127.0.0.1:9092" server_app_address="127.0.0.1:9091" db_arg="--database :flwr-in-memory-state:" - server_arg="--ssl-ca-certfile certificates/ca.crt --ssl-certfile certificates/server.pem --ssl-keyfile certificates/server.key" - client_arg="--root-certificates certificates/ca.crt" server_auth="--auth-list-public-keys keys/client_public_keys.csv --auth-superlink-private-key keys/server_credentials --auth-superlink-public-key keys/server_credentials.pub" client_auth_1="--auth-supernode-private-key keys/client_credentials_1 --auth-supernode-public-key keys/client_credentials_1.pub" client_auth_2="--auth-supernode-private-key keys/client_credentials_2 --auth-supernode-public-key keys/client_credentials_2.pub" From 84b4fd7c8da19a1ef042c4b9a0aea85cfba19ea1 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Sat, 7 Sep 2024 09:46:27 +0100 Subject: [PATCH 177/188] fix(benchmarks:skip) Fix a git clone depth issue for generalNLP challenge (#4155) --- benchmarks/flowertune-llm/evaluation/general-nlp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/flowertune-llm/evaluation/general-nlp/README.md b/benchmarks/flowertune-llm/evaluation/general-nlp/README.md index 51c801494f6d..18666968108d 100644 --- a/benchmarks/flowertune-llm/evaluation/general-nlp/README.md +++ b/benchmarks/flowertune-llm/evaluation/general-nlp/README.md @@ -23,7 +23,7 @@ huggingface-cli login Download data from [FastChat](https://github.com/lm-sys/FastChat): ```shell -git clone --depth=1 https://github.com/lm-sys/FastChat.git && cd FastChat && git checkout d561f87b24de197e25e3ddf7e09af93ced8dfe36 && mv fastchat/llm_judge/data ../data && cd .. && rm -rf FastChat +git clone https://github.com/lm-sys/FastChat.git && cd FastChat && git checkout d561f87b24de197e25e3ddf7e09af93ced8dfe36 && mv fastchat/llm_judge/data ../data && cd .. && rm -rf FastChat ``` From 0e7c1b06c32ab90e0d3cf64825ed51eedd715509 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Sat, 7 Sep 2024 09:53:22 +0100 Subject: [PATCH 178/188] fix(benchmarks:skip) Update accuracy values for code challenge (#4157) --- benchmarks/flowertune-llm/evaluation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/flowertune-llm/evaluation/README.md b/benchmarks/flowertune-llm/evaluation/README.md index 1b6383df296a..d7216c089d8a 100644 --- a/benchmarks/flowertune-llm/evaluation/README.md +++ b/benchmarks/flowertune-llm/evaluation/README.md @@ -37,7 +37,7 @@ The default template generated by `flwr new` (see the [Project Creation Instruct | | MBPP | HumanEval | MultiPL-E (JS) | MultiPL-E (C++) | Avg | |:----------:|:-----:|:---------:|:--------------:|:---------------:|:-----:| -| Pass@1 (%) | 32.60 | 26.83 | 29.81 | 24.22 | 28.37 | +| Pass@1 (%) | 31.60 | 23.78 | 28.57 | 25.47 | 27.36 | ## Make submission on FlowerTune LLM Leaderboard From 9afe0f83652b5fe67cab6f76cf0655622ff48689 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Mon, 9 Sep 2024 09:02:51 +0100 Subject: [PATCH 179/188] ci(*:skip) Remove unnecessary pip install in e2e test --- .github/workflows/e2e.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 53015310dcc1..815d6422848b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -146,8 +146,6 @@ jobs: if: ${{ github.repository == 'adap/flower' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} run: | python -m pip install https://${{ env.ARTIFACT_BUCKET }}/py/${{ needs.wheel.outputs.dir }}/${{ needs.wheel.outputs.short_sha }}/${{ needs.wheel.outputs.whl_path }} - - name: Install e2e components - run: pip install . - name: Download dataset if: ${{ matrix.dataset }} run: python -c "${{ matrix.dataset }}" From c1af98fb12e27c1411840d8d91a92192984829f3 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Mon, 9 Sep 2024 11:53:48 +0200 Subject: [PATCH 180/188] fix(framework:skip) Check list length before access (#4159) --- src/py/flwr/client/client_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/flwr/client/client_app.py b/src/py/flwr/client/client_app.py index 2a913b3a248d..5e76acd1ddd8 100644 --- a/src/py/flwr/client/client_app.py +++ b/src/py/flwr/client/client_app.py @@ -41,11 +41,11 @@ def _alert_erroneous_client_fn() -> None: def _inspect_maybe_adapt_client_fn_signature(client_fn: ClientFnExt) -> ClientFnExt: client_fn_args = inspect.signature(client_fn).parameters - first_arg = list(client_fn_args.keys())[0] if len(client_fn_args) != 1: _alert_erroneous_client_fn() + first_arg = list(client_fn_args.keys())[0] first_arg_type = client_fn_args[first_arg].annotation if first_arg_type is str or first_arg == "cid": From 00b384b4461dfc30c0c33ad987caf47d6dbce190 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Mon, 9 Sep 2024 11:59:58 +0200 Subject: [PATCH 181/188] docs(framework:skip) Add FLWR_VERSION_REF to docs (#4154) Signed-off-by: Robert Steiner --- ...contributor-how-to-build-docker-images.rst | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/source/contributor-how-to-build-docker-images.rst b/doc/source/contributor-how-to-build-docker-images.rst index 522d124dfd9b..d6acad4afa03 100644 --- a/doc/source/contributor-how-to-build-docker-images.rst +++ b/doc/source/contributor-how-to-build-docker-images.rst @@ -26,7 +26,7 @@ Before we can start, we need to meet a few prerequisites in our local developmen default values, others must be specified when building the image. All available build arguments for each image are listed in one of the tables below. -Building the base image +Building the Base Image ----------------------- .. list-table:: @@ -65,6 +65,10 @@ Building the base image - The Flower package to be installed. - No - ``flwr`` or ``flwr-nightly`` + * - ``FLWR_VERSION_REF`` + - A `direct reference `_ without the ``@`` specifier. If both ``FLWR_VERSION`` and ``FLWR_VERSION_REF`` are specified, the ``FLWR_VERSION_REF`` has precedence. + - No + - `Direct Reference Examples`_ The following example creates a base Ubuntu/Alpine image with Python ``3.11.0``, pip :substitution-code:`|pip_version|`, setuptools :substitution-code:`|setuptools_version|` @@ -84,8 +88,8 @@ and Flower :substitution-code:`|stable_flwr_version|`: In this example, we specify our image name as ``flwr_base`` and the tag as ``0.1.0``. Remember that the build arguments as well as the name and tag can be adapted to your needs. These values serve as examples only. -Building the SuperLink/SuperNode or ServerApp image ---------------------------------------------------- +Building a Flower Binary Image +------------------------------ .. list-table:: :widths: 25 45 15 15 @@ -130,3 +134,21 @@ After creating the image, we can test whether the image is working: .. code-block:: bash $ docker run --rm flwr_superlink:0.1.0 --help + +Direct Reference Examples +------------------------- + +.. code-block:: bash + :substitutions: + + # main branch + git+https://github.com/adap/flower.git@main + + # commit hash + git+https://github.com/adap/flower.git@1187c707f1894924bfa693d99611cf6f93431835 + + # tag + git+https://github.com/adap/flower.git@|stable_flwr_version| + + # artifact store + https://artifact.flower.ai/py/main/latest/flwr-|stable_flwr_version|-py3-none-any.whl From ec760b0231705129d663da67bd9edd9bc141d2e1 Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Mon, 9 Sep 2024 18:20:07 +0100 Subject: [PATCH 182/188] feat(benchmarks) Add LLM evaluation pipeline for Medical challenge (#3768) Co-authored-by: jafermarq --- .../evaluation/medical/README.md | 38 ++++ .../evaluation/medical/benchmarks.py | 174 ++++++++++++++++++ .../flowertune-llm/evaluation/medical/eval.py | 62 +++++++ .../evaluation/medical/requirements.txt | 7 + .../evaluation/medical/utils.py | 81 ++++++++ 5 files changed, 362 insertions(+) create mode 100644 benchmarks/flowertune-llm/evaluation/medical/README.md create mode 100644 benchmarks/flowertune-llm/evaluation/medical/benchmarks.py create mode 100644 benchmarks/flowertune-llm/evaluation/medical/eval.py create mode 100644 benchmarks/flowertune-llm/evaluation/medical/requirements.txt create mode 100644 benchmarks/flowertune-llm/evaluation/medical/utils.py diff --git a/benchmarks/flowertune-llm/evaluation/medical/README.md b/benchmarks/flowertune-llm/evaluation/medical/README.md new file mode 100644 index 000000000000..78de069460d8 --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/medical/README.md @@ -0,0 +1,38 @@ +# Evaluation for Medical challenge + +We build up a medical question answering (QA) pipeline to evaluate our fined-tuned LLMs. +Three datasets have been selected for this evaluation: [PubMedQA](https://huggingface.co/datasets/bigbio/pubmed_qa), [MedMCQA](https://huggingface.co/datasets/medmcqa), and [MedQA](https://huggingface.co/datasets/bigbio/med_qa). + + +## Environment Setup + +```shell +git clone --depth=1 https://github.com/adap/flower.git && mv flower/benchmarks/flowertune-llm/evaluation/medical ./flowertune-eval-medical && rm -rf flower && cd flowertune-eval-medical +``` + +Create a new Python environment (we recommend Python 3.10), activate it, then install dependencies with: + +```shell +# From a new python environment, run: +pip install -r requirements.txt + +# Log in HuggingFace account +huggingface-cli login +``` + +## Generate model decision & calculate accuracy + +```bash +python eval.py \ +--peft-path=/path/to/fine-tuned-peft-model-dir/ \ # e.g., ./peft_1 +--run-name=fl \ # specified name for this run +--batch-size=16 \ +--quantization=4 \ +--datasets=pubmedqa,medmcqa,medqa +``` + +The model answers and accuracy values will be saved to `benchmarks/generation_{dataset_name}_{run_name}.jsonl` and `benchmarks/acc_{dataset_name}_{run_name}.txt`, respectively. + + +> [!NOTE] +> Please ensure that you provide all **three accuracy values (PubMedQA, MedMCQA, MedQA)** for three evaluation datasets when submitting to the LLM Leaderboard (see the [`Make Submission`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation#make-submission-on-flowertune-llm-leaderboard) section). diff --git a/benchmarks/flowertune-llm/evaluation/medical/benchmarks.py b/benchmarks/flowertune-llm/evaluation/medical/benchmarks.py new file mode 100644 index 000000000000..c72e2a7894da --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/medical/benchmarks.py @@ -0,0 +1,174 @@ +import json + +import pandas as pd +from sklearn.metrics import accuracy_score +from torch.utils.data import DataLoader +from tqdm import tqdm +from utils import format_answer, format_example, save_results + +import datasets + +# The instructions refer to Meditron evaluation: +# https://github.com/epfLLM/meditron/blob/main/evaluation/instructions.json +INSTRUCTIONS = { + "pubmedqa": "As an expert doctor in clinical science and medical knowledge, can you tell me if the following statement is correct? Answer yes, no, or maybe.", + "medqa": "You are a medical doctor taking the US Medical Licensing Examination. You need to demonstrate your understanding of basic and clinical science, medical knowledge, and mechanisms underlying health, disease, patient care, and modes of therapy. Show your ability to apply the knowledge essential for medical practice. For the following multiple-choice question, select one correct answer from A to E. Base your answer on the current and standard practices referenced in medical guidelines.", + "medmcqa": "You are a medical doctor answering realworld medical entrance exam questions. Based on your understanding of basic and clinical science, medical knowledge, and mechanisms underlying health, disease, patient care, and modes of therapy, answer the following multiple-choice question. Select one correct answer from A to D. Base your answer on the current and standard practices referenced in medical guidelines.", +} + + +def infer_pubmedqa(model, tokenizer, batch_size, run_name): + name = "pubmedqa" + answer_type = "boolean" + dataset = datasets.load_dataset( + "bigbio/pubmed_qa", + "pubmed_qa_labeled_fold0_source", + split="test", + trust_remote_code=True, + ) + # Post process + instruction = INSTRUCTIONS[name] + + def post_process(row): + context = "\n".join(row["CONTEXTS"]) + row["prompt"] = f"{context}\n{row['QUESTION']}" + row["gold"] = row["final_decision"] + row["long_answer"] = row["LONG_ANSWER"] + row["prompt"] = f"{instruction}\n{row['prompt']}\nThe answer is:\n" + return row + + dataset = dataset.map(post_process) + + # Generate results + generate_results(name, run_name, dataset, model, tokenizer, batch_size, answer_type) + + +def infer_medqa(model, tokenizer, batch_size, run_name): + name = "medqa" + answer_type = "mcq" + dataset = datasets.load_dataset( + "bigbio/med_qa", + "med_qa_en_4options_source", + split="test", + trust_remote_code=True, + ) + + # Post process + instruction = INSTRUCTIONS[name] + + def post_process(row): + choices = [opt["value"] for opt in row["options"]] + row["prompt"] = format_example(row["question"], choices) + for opt in row["options"]: + if opt["value"] == row["answer"]: + row["gold"] = opt["key"] + break + row["prompt"] = f"{instruction}\n{row['prompt']}\nThe answer is:\n" + return row + + dataset = dataset.map(post_process) + + # Generate results + generate_results(name, run_name, dataset, model, tokenizer, batch_size, answer_type) + + +def infer_medmcqa(model, tokenizer, batch_size, run_name): + name = "medmcqa" + answer_type = "mcq" + dataset = datasets.load_dataset( + "medmcqa", split="validation", trust_remote_code=True + ) + + # Post process + instruction = INSTRUCTIONS[name] + + def post_process(row): + options = [row["opa"], row["opb"], row["opc"], row["opd"]] + answer = int(row["cop"]) + row["prompt"] = format_example(row["question"], options) + row["gold"] = chr(ord("A") + answer) if answer in [0, 1, 2, 3] else None + row["prompt"] = f"{instruction}\n{row['prompt']}\nThe answer is:\n" + return row + + dataset = dataset.map(post_process) + + # Generate results + generate_results(name, run_name, dataset, model, tokenizer, batch_size, answer_type) + + +def generate_results( + name, run_name, dataset, model, tokenizer, batch_size, answer_type +): + # Run inference + prediction = inference(dataset, model, tokenizer, batch_size) + + # Calculate accuracy + acc = accuracy_compute(prediction, answer_type) + + # Save results and generations + save_results(name, run_name, prediction, acc) + + +def inference(dataset, model, tokenizer, batch_size): + columns_process = ["prompt", "gold"] + dataset_process = pd.DataFrame(dataset, columns=dataset.features)[columns_process] + dataset_process = dataset_process.assign(output="Null") + temperature = 1.0 + + inference_data = json.loads(dataset_process.to_json(orient="records")) + data_loader = DataLoader(inference_data, batch_size=batch_size, shuffle=False) + + batch_counter = 0 + for batch in tqdm(data_loader, total=len(data_loader), position=0, leave=True): + prompts = [ + f"<|im_start|>question\n{prompt}<|im_end|>\n<|im_start|>answer\n" + for prompt in batch["prompt"] + ] + if batch_counter == 0: + print(prompts[0]) + + # Process tokenizer + stop_seq = ["###"] + if tokenizer.eos_token is not None: + stop_seq.append(tokenizer.eos_token) + if tokenizer.pad_token is not None: + stop_seq.append(tokenizer.pad_token) + max_new_tokens = len( + tokenizer(batch["gold"][0], add_special_tokens=False)["input_ids"] + ) + + outputs = [] + for prompt in prompts: + input_ids = tokenizer.encode(prompt, return_tensors="pt").to("cuda") + output_ids = model.generate( + inputs=input_ids, + max_new_tokens=max_new_tokens, + do_sample=False, + top_p=1.0, + temperature=temperature, + pad_token_id=tokenizer.eos_token_id, + ) + output_ids = output_ids[0][len(input_ids[0]) :] + output = tokenizer.decode(output_ids, skip_special_tokens=True) + outputs.append(output) + + for prompt, out in zip(batch["prompt"], outputs): + dataset_process.loc[dataset_process["prompt"] == prompt, "output"] = out + batch_counter += 1 + + return dataset_process + + +def accuracy_compute(dataset, answer_type): + dataset = json.loads(dataset.to_json(orient="records")) + preds, golds = [], [] + for row in dataset: + answer = row["gold"].lower() + output = row["output"].lower() + pred, gold = format_answer(output, answer, answer_type=answer_type) + preds.append(pred) + golds.append(gold) + + accuracy = accuracy_score(preds, golds) + + return accuracy diff --git a/benchmarks/flowertune-llm/evaluation/medical/eval.py b/benchmarks/flowertune-llm/evaluation/medical/eval.py new file mode 100644 index 000000000000..7405e1493e4d --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/medical/eval.py @@ -0,0 +1,62 @@ +import argparse + +import torch +from peft import PeftModel +from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig + +from benchmarks import infer_medmcqa, infer_medqa, infer_pubmedqa + +# Fixed seed +torch.manual_seed(2024) + +parser = argparse.ArgumentParser() +parser.add_argument( + "--base-model-name-path", type=str, default="mistralai/Mistral-7B-v0.3" +) +parser.add_argument("--run-name", type=str, default="fl") +parser.add_argument("--peft-path", type=str, default=None) +parser.add_argument( + "--datasets", + type=str, + default="pubmedqa", + help="The dataset to infer on: [pubmedqa, medqa, medmcqa]", +) +parser.add_argument("--batch-size", type=int, default=16) +parser.add_argument("--quantization", type=int, default=4) +args = parser.parse_args() + + +# Load model and tokenizer +if args.quantization == 4: + quantization_config = BitsAndBytesConfig(load_in_4bit=True) + torch_dtype = torch.float32 +elif args.quantization == 8: + quantization_config = BitsAndBytesConfig(load_in_8bit=True) + torch_dtype = torch.float16 +else: + raise ValueError( + f"Use 4-bit or 8-bit quantization. You passed: {args.quantization}/" + ) + +model = AutoModelForCausalLM.from_pretrained( + args.base_model_name_path, + quantization_config=quantization_config, + torch_dtype=torch_dtype, +) +if args.peft_path is not None: + model = PeftModel.from_pretrained( + model, args.peft_path, torch_dtype=torch_dtype + ).to("cuda") + +tokenizer = AutoTokenizer.from_pretrained(args.base_model_name_path) + +# Evaluate +for dataset in args.datasets.split(","): + if dataset == "pubmedqa": + infer_pubmedqa(model, tokenizer, args.batch_size, args.run_name) + elif dataset == "medqa": + infer_medqa(model, tokenizer, args.batch_size, args.run_name) + elif dataset == "medmcqa": + infer_medmcqa(model, tokenizer, args.batch_size, args.run_name) + else: + raise ValueError("Undefined Dataset.") diff --git a/benchmarks/flowertune-llm/evaluation/medical/requirements.txt b/benchmarks/flowertune-llm/evaluation/medical/requirements.txt new file mode 100644 index 000000000000..adfc8b0c59db --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/medical/requirements.txt @@ -0,0 +1,7 @@ +peft==0.6.2 +pandas==2.2.2 +scikit-learn==1.5.0 +datasets==2.20.0 +sentencepiece==0.2.0 +protobuf==5.27.1 +bitsandbytes==0.43.1 diff --git a/benchmarks/flowertune-llm/evaluation/medical/utils.py b/benchmarks/flowertune-llm/evaluation/medical/utils.py new file mode 100644 index 000000000000..44d0763d39d4 --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/medical/utils.py @@ -0,0 +1,81 @@ +import os +import re + + +def format_example(question, choices): + if not question.endswith("?") and not question.endswith("."): + question += "?" + options_str = "\n".join([f"{chr(65+i)}. {choices[i]}" for i in range(len(choices))]) + prompt = "Question: " + question + "\n\nOptions:\n" + options_str + return prompt + + +def save_results(dataset_name, run_name, dataset, acc): + path = "./benchmarks/" + if not os.path.exists(path): + os.makedirs(path) + + # Save results + results_path = os.path.join(path, f"acc_{dataset_name}_{run_name}.txt") + with open(results_path, "w") as f: + f.write(f"Accuracy: {acc}. ") + print(f"Accuracy: {acc}. ") + + # Save generations + generation_path = os.path.join(path, f"generation_{dataset_name}_{run_name}.jsonl") + dataset.to_json(generation_path, orient="records") + + +def format_answer(output_full, answer, answer_type="mcq"): + output = output_full + default = (output_full, answer) + if "\n##" in output: + try: + output = output.split("\n##")[1].split("\n")[0].strip().lower() + except Exception: + return default + if "###" in answer: + try: + answer = answer.split("answer is:")[1].split("###")[0].strip() + except Exception: + return default + + output = re.sub(r"[^a-zA-Z0-9]", " ", output).strip() + output = re.sub(" +", " ", output) + + if answer_type == "boolean": + output = clean_boolean_answer(output) + elif answer_type == "mcq": + output = clean_mcq_answer(output) + + if output in ["a", "b", "c", "d", "e", "yes", "no"]: + return output, answer + else: + return default + + +def clean_mcq_answer(output): + output = clean_answer(output) + try: + output = output[0] + except Exception: + return output + return output + + +def clean_boolean_answer(output): + if "yesyes" in output: + output = output.replace("yesyes", "yes") + elif "nono" in output: + output = output.replace("nono", "no") + elif "yesno" in output: + output = output.replace("yesno", "yes") + elif "noyes" in output: + output = output.replace("noyes", "no") + output = clean_answer(output) + return output + + +def clean_answer(output): + output_clean = output.encode("ascii", "ignore").decode("ascii") + return output_clean From 8f8639f1c9087e1887e3b66f418440faa016d5f7 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Mon, 9 Sep 2024 23:28:54 +0200 Subject: [PATCH 183/188] feat(framework) Build Docker images from main branch (#4153) Signed-off-by: Robert Steiner Co-authored-by: Taner Topal --- .github/workflows/docker-build-main.yml | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/docker-build-main.yml diff --git a/.github/workflows/docker-build-main.yml b/.github/workflows/docker-build-main.yml new file mode 100644 index 000000000000..81ef845eae29 --- /dev/null +++ b/.github/workflows/docker-build-main.yml @@ -0,0 +1,69 @@ +name: Build Docker Images Main Branch + +on: + push: + branches: + - 'main' + +jobs: + parameters: + if: github.repository == 'adap/flower' + name: Collect docker build parameters + runs-on: ubuntu-22.04 + timeout-minutes: 10 + outputs: + pip-version: ${{ steps.versions.outputs.pip-version }} + setuptools-version: ${{ steps.versions.outputs.setuptools-version }} + flwr-version-ref: ${{ steps.versions.outputs.flwr-version-ref }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - uses: ./.github/actions/bootstrap + id: bootstrap + + - id: versions + run: | + echo "pip-version=${{ steps.bootstrap.outputs.pip-version }}" >> "$GITHUB_OUTPUT" + echo "setuptools-version=${{ steps.bootstrap.outputs.setuptools-version }}" >> "$GITHUB_OUTPUT" + echo "flwr-version-ref=git+${{ github.server_url }}/${{ github.repository }}.git@${{ github.sha }}" >> "$GITHUB_OUTPUT" + + build-docker-base-images: + name: Build base images + if: github.repository == 'adap/flower' + uses: ./.github/workflows/_docker-build.yml + needs: parameters + with: + namespace-repository: flwr/base + file-dir: src/docker/base/ubuntu + build-args: | + PIP_VERSION=${{ needs.parameters.outputs.pip-version }} + SETUPTOOLS_VERSION=${{ needs.parameters.outputs.setuptools-version }} + FLWR_VERSION_REF=${{ needs.parameters.outputs.flwr-version-ref }} + tags: unstable + secrets: + dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + + build-docker-binary-images: + name: Build binary images + if: github.repository == 'adap/flower' + uses: ./.github/workflows/_docker-build.yml + needs: build-docker-base-images + strategy: + fail-fast: false + matrix: + images: [ + { repository: "flwr/superlink", file_dir: "src/docker/superlink" }, + { repository: "flwr/supernode", file_dir: "src/docker/supernode" }, + { repository: "flwr/serverapp", file_dir: "src/docker/serverapp" }, + { repository: "flwr/superexec", file_dir: "src/docker/superexec" }, + { repository: "flwr/clientapp", file_dir: "src/docker/clientapp" } + ] + with: + namespace-repository: ${{ matrix.images.repository }} + file-dir: ${{ matrix.images.file_dir }} + build-args: BASE_IMAGE=unstable + tags: unstable + secrets: + dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} From 375d259c769385b4e57156050702eef482a92c26 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 10 Sep 2024 11:36:18 +0100 Subject: [PATCH 184/188] fix(framework:skip) Specify keyword `deterministic` in `SerializeToString` method (#4163) --- .../grpc_rere_client/client_interceptor.py | 5 ++--- .../client_interceptor_test.py | 2 +- .../fleet/grpc_rere/server_interceptor.py | 3 ++- .../grpc_rere/server_interceptor_test.py | 22 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor.py b/src/py/flwr/client/grpc_rere_client/client_interceptor.py index c16f911eb4c2..8e8b701ca272 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor.py @@ -130,13 +130,12 @@ def intercept_unary_unary( if self.shared_secret is None: raise RuntimeError("Failure to compute hmac") + message_bytes = request.SerializeToString(deterministic=True) metadata.append( ( _AUTH_TOKEN_HEADER, base64.urlsafe_b64encode( - compute_hmac( - self.shared_secret, request.SerializeToString(True) - ) + compute_hmac(self.shared_secret, message_bytes) ), ) ) diff --git a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py index 155bae202720..72ac20738ad6 100644 --- a/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py +++ b/src/py/flwr/client/grpc_rere_client/client_interceptor_test.py @@ -73,7 +73,7 @@ def unary_unary( """Handle unary call.""" with self._lock: self._received_client_metadata = context.invocation_metadata() - self._received_message_bytes = request.SerializeToString(True) + self._received_message_bytes = request.SerializeToString(deterministic=True) if isinstance(request, CreateNodeRequest): context.send_initial_metadata( diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py index 70b38f8b625e..2c58d0049849 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor.py @@ -188,7 +188,8 @@ def _verify_hmac( self, public_key: ec.EllipticCurvePublicKey, request: Request, hmac_value: bytes ) -> bool: shared_secret = generate_shared_key(self.server_private_key, public_key) - return verify_hmac(shared_secret, request.SerializeToString(True), hmac_value) + message_bytes = request.SerializeToString(deterministic=True) + return verify_hmac(shared_secret, message_bytes, hmac_value) def _create_authenticated_node( self, diff --git a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py index 74914be68a8f..ec7a775a5dc3 100644 --- a/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py +++ b/src/py/flwr/server/superlink/fleet/grpc_rere/server_interceptor_test.py @@ -166,7 +166,7 @@ def test_successful_delete_node_with_metadata(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -195,7 +195,7 @@ def test_unsuccessful_delete_node_with_metadata(self) -> None: node_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -222,7 +222,7 @@ def test_successful_pull_task_ins_with_metadata(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -251,7 +251,7 @@ def test_unsuccessful_pull_task_ins_with_metadata(self) -> None: node_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -280,7 +280,7 @@ def test_successful_push_task_res_with_metadata(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -311,7 +311,7 @@ def test_unsuccessful_push_task_res_with_metadata(self) -> None: node_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -339,7 +339,7 @@ def test_successful_get_run_with_metadata(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -369,7 +369,7 @@ def test_unsuccessful_get_run_with_metadata(self) -> None: node_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -396,7 +396,7 @@ def test_successful_ping_with_metadata(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -425,7 +425,7 @@ def test_unsuccessful_ping_with_metadata(self) -> None: node_private_key, _ = generate_key_pairs() shared_secret = generate_shared_key(node_private_key, self._server_public_key) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) @@ -469,7 +469,7 @@ def test_successful_restore_node(self) -> None: self._node_private_key, self._server_public_key ) hmac_value = base64.urlsafe_b64encode( - compute_hmac(shared_secret, request.SerializeToString(True)) + compute_hmac(shared_secret, request.SerializeToString(deterministic=True)) ) public_key_bytes = base64.urlsafe_b64encode( public_key_to_bytes(self._node_public_key) From 1dfa2295e0d490d64a1a12ba56e75258df042d26 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 10 Sep 2024 12:36:51 +0100 Subject: [PATCH 185/188] feat(framework) Implement `keys/values/items` methods for `TypedDict` (#4146) --- src/py/flwr/common/record/typeddict.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/py/flwr/common/record/typeddict.py b/src/py/flwr/common/record/typeddict.py index 791077d8eff2..37d98b01a306 100644 --- a/src/py/flwr/common/record/typeddict.py +++ b/src/py/flwr/common/record/typeddict.py @@ -15,7 +15,18 @@ """Typed dict base class for *Records.""" -from typing import Callable, Dict, Generic, Iterator, MutableMapping, TypeVar, cast +from typing import ( + Callable, + Dict, + Generic, + ItemsView, + Iterator, + KeysView, + MutableMapping, + TypeVar, + ValuesView, + cast, +) K = TypeVar("K") # Key type V = TypeVar("V") # Value type @@ -73,3 +84,15 @@ def __eq__(self, other: object) -> bool: if isinstance(other, dict): return data == other return NotImplemented + + def keys(self) -> KeysView[K]: + """D.keys() -> a set-like object providing a view on D's keys.""" + return cast(Dict[K, V], self.__dict__["_data"]).keys() + + def values(self) -> ValuesView[V]: + """D.values() -> an object providing a view on D's values.""" + return cast(Dict[K, V], self.__dict__["_data"]).values() + + def items(self) -> ItemsView[K, V]: + """D.items() -> a set-like object providing a view on D's items.""" + return cast(Dict[K, V], self.__dict__["_data"]).items() From a8a5fd3b0210c0fdbbb163c9496efa61a3afae2f Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 10 Sep 2024 14:11:11 +0200 Subject: [PATCH 186/188] fix(framework:skip) Update docstrings examples in `RecordSet` (#4133) --- src/py/flwr/client/client_app.py | 2 +- src/py/flwr/common/record/recordset.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/client/client_app.py b/src/py/flwr/client/client_app.py index 5e76acd1ddd8..c322ba747114 100644 --- a/src/py/flwr/client/client_app.py +++ b/src/py/flwr/client/client_app.py @@ -263,7 +263,7 @@ def _registration_error(fn_name: str) -> ValueError: >>> class FlowerClient(NumPyClient): >>> # ... >>> - >>> def client_fn(cid) -> Client: + >>> def client_fn(context: Context): >>> return FlowerClient().to_client() >>> >>> app = ClientApp( diff --git a/src/py/flwr/common/record/recordset.py b/src/py/flwr/common/record/recordset.py index f16a22695d6e..b2d1da4411bb 100644 --- a/src/py/flwr/common/record/recordset.py +++ b/src/py/flwr/common/record/recordset.py @@ -119,7 +119,7 @@ class RecordSet: Let's see an example. >>> from flwr.common import RecordSet - >>> from flwr.common import ConfigsRecords, MetricsRecords, ParametersRecord + >>> from flwr.common import ConfigsRecord, MetricsRecord, ParametersRecord >>> >>> # Let's begin with an empty record >>> my_recordset = RecordSet() From c71a73c01e347e83a49ac9d9aab182000bd81f8c Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Tue, 10 Sep 2024 16:14:20 +0100 Subject: [PATCH 187/188] refactor(framework:skip) Remove `get_parameters` from `flwr` TensorFlow template (#4167) --- .../flwr/cli/new/templates/app/code/client.tensorflow.py.tpl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl index 48ee3b4f5356..f8c148691561 100644 --- a/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +++ b/src/py/flwr/cli/new/templates/app/code/client.tensorflow.py.tpl @@ -17,9 +17,6 @@ class FlowerClient(NumPyClient): self.batch_size = batch_size self.verbose = verbose - def get_parameters(self, config): - return self.model.get_weights() - def fit(self, parameters, config): self.model.set_weights(parameters) self.model.fit( From 801c0fc3394eedcc579960efb8cf278867568dbe Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Tue, 10 Sep 2024 18:22:51 +0100 Subject: [PATCH 188/188] feat(benchmarks) Add LLM evaluation pipeline for Code challenge (#3801) Co-authored-by: jafermarq --- .../flowertune-llm/evaluation/code/README.md | 70 +++++++++++++++++++ .../evaluation/code/requirements.txt | 7 ++ 2 files changed, 77 insertions(+) create mode 100644 benchmarks/flowertune-llm/evaluation/code/README.md create mode 100644 benchmarks/flowertune-llm/evaluation/code/requirements.txt diff --git a/benchmarks/flowertune-llm/evaluation/code/README.md b/benchmarks/flowertune-llm/evaluation/code/README.md new file mode 100644 index 000000000000..fd63ced2f1e2 --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/code/README.md @@ -0,0 +1,70 @@ +# Evaluation for Code challenge + +We leverage the code generation evaluation metrics provided by [bigcode-evaluation-harness](https://github.com/bigcode-project/bigcode-evaluation-harness/tree/main) to evaluate our fine-tuned LLMs. +Three datasets have been selected for this evaluation: [MBPP](https://huggingface.co/datasets/google-research-datasets/mbpp) (Python), [HumanEval](https://huggingface.co/datasets/openai/openai_humaneval) (Python), and [MultiPL-E](https://github.com/nuprl/MultiPL-E) (JavaScript, C++). + +> [!WARNING] +> The evaluation process takes ~30 GB VRAM. On a 40GB A100 it requires 15-30mins depending on the dataset to complete. + +## Environment Setup + +```shell +git clone --depth=1 https://github.com/adap/flower.git && mv flower/benchmarks/flowertune-llm/evaluation/code ./flowertune-eval-code && rm -rf flower && cd flowertune-eval-code +``` + +Create a new Python environment (we recommend Python 3.10), activate it, then install dependencies with: + +```shell +# From a new python environment, run: +pip install -r requirements.txt + +# Log in HuggingFace account +huggingface-cli login +``` + +After that, install `Node.js` and `g++` for the evaluation of JavaScript, C++: + +```shell +# Install nvm (Node Version Manager) +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + +# Restart your terminal + +# Download and install Node.js (you may need to restart the terminal) +nvm install 20 + +# Install g++ +sudo apt-get install g++ +``` + +Then, download the `main.py` script from `bigcode-evaluation-harness` repository. + +```shell +git clone https://github.com/bigcode-project/bigcode-evaluation-harness.git && cd bigcode-evaluation-harness && git checkout 0f3e95f0806e78a4f432056cdb1be93604a51d69 && mv main.py ../ && cd .. && rm -rf bigcode-evaluation-harness +``` + + +## Generate model answers & calculate pass@1 score + +> [!NOTE] +> Evaluation needs to be run on MBPP, HumanEval, MultiPL-E (JS) and MultiPL-E (C++). + +```bash +python main.py \ +--model=mistralai/Mistral-7B-v0.3 \ +--peft_model=/path/to/fine-tuned-peft-model-dir/ \ # e.g., ./peft_1 +--max_length_generation=1024 \ # change to 2048 when running mbpp +--batch_size=4 \ +--use_auth_token \ +--allow_code_execution \ +--save_generations \ +--save_references \ +--tasks=humaneval \ # chosen from [mbpp, humaneval, multiple-js, multiple-cpp] +--metric_output_path=./evaluation_results_humaneval.json # change dataset name based on your choice +``` + +The model answers and pass@1 scores will be saved to `generations_{dataset_name}.json` and `evaluation_results_{dataset_name}.json`, respectively. + + +> [!NOTE] +> Please ensure that you provide all **four pass@1 scores** for the evaluation datasets when submitting to the LLM Leaderboard (see the [`Make Submission`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation#make-submission-on-flowertune-llm-leaderboard) section). diff --git a/benchmarks/flowertune-llm/evaluation/code/requirements.txt b/benchmarks/flowertune-llm/evaluation/code/requirements.txt new file mode 100644 index 000000000000..74b5b79d634d --- /dev/null +++ b/benchmarks/flowertune-llm/evaluation/code/requirements.txt @@ -0,0 +1,7 @@ +peft==0.6.2 +datasets==2.20.0 +evaluate==0.3.0 +sentencepiece==0.2.0 +protobuf==5.27.1 +bitsandbytes==0.43.1 +git+https://github.com/bigcode-project/bigcode-evaluation-harness.git@0f3e95f0806e78a4f432056cdb1be93604a51d69

    Hy7mHcs60rU|3$vEd=*S7hBe)R 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