From 99f5801615dcdcc099965ea4d9bfb19bae9e9d20 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 17 Sep 2024 15:36:10 +0100 Subject: [PATCH 01/16] refactor(examples) Update `tensorflow-privacy` example (#4204) Co-authored-by: jafermarq --- examples/tensorflow-privacy/README.md | 62 ++++---- examples/tensorflow-privacy/client.py | 150 ------------------ examples/tensorflow-privacy/pyproject.toml | 29 +++- examples/tensorflow-privacy/server.py | 22 --- .../tensorflow-privacy/tf_privacy/__init__.py | 1 + .../tf_privacy/client_app.py | 93 +++++++++++ .../tf_privacy/server_app.py | 31 ++++ .../tensorflow-privacy/tf_privacy/task.py | 52 ++++++ 8 files changed, 230 insertions(+), 210 deletions(-) delete mode 100644 examples/tensorflow-privacy/client.py delete mode 100644 examples/tensorflow-privacy/server.py create mode 100644 examples/tensorflow-privacy/tf_privacy/__init__.py create mode 100644 examples/tensorflow-privacy/tf_privacy/client_app.py create mode 100644 examples/tensorflow-privacy/tf_privacy/server_app.py create mode 100644 examples/tensorflow-privacy/tf_privacy/task.py diff --git a/examples/tensorflow-privacy/README.md b/examples/tensorflow-privacy/README.md index 8156f92f60c9..af85865346bb 100644 --- a/examples/tensorflow-privacy/README.md +++ b/examples/tensorflow-privacy/README.md @@ -1,66 +1,64 @@ --- -tags: [basic, vision, fds, privacy, dp] +tags: [DP, DP-SGD, basic, vision, fds, privacy] dataset: [MNIST] framework: [tensorflow] --- # Training with Sample-Level Differential Privacy using TensorFlow-Privacy Engine -In this example, we demonstrate how to train a model with sample-level differential privacy (DP) using Flower. We employ TensorFlow and integrate the tensorflow-privacy Engine to achieve sample-level differential privacy. This setup ensures robust privacy guarantees during the client training phase. +In this example, we demonstrate how to train a model with sample-level differential privacy (DP) using Flower. We employ TensorFlow and integrate the tensorflow-privacy engine to achieve sample-level differential privacy. This setup ensures robust privacy guarantees during the client training phase. For more information about DP in Flower please refer to the [tutorial](https://flower.ai/docs/framework/how-to-use-differential-privacy.html). For additional information about tensorflow-privacy, visit the official [website](https://www.tensorflow.org/responsible_ai/privacy/guide). -## 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/tensorflow-privacy . && rm -rf flower && cd tensorflow-privacy +git clone --depth=1 https://github.com/adap/flower.git \ + && mv flower/examples/tensorflow-privacy . \ + && rm -rf flower \ + && cd tensorflow-privacy ``` This will create a new directory called `tensorflow-privacy` containing the following files: ```shell --- pyproject.toml --- client.py --- server.py --- README.md +tensorflow-privacy +├── tf_privacy +│ ├── 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 are defined in `pyproject.toml`. Install them with: - -```shell -pip install . -``` +> \[!NOTE\] +> Please note that, at the current state, users cannot set `NodeConfig` for simulated `ClientApp`s. For this reason, the hyperparameter `noise_multiplier` is set in the `client_fn` method based on a condition check on `partition_id`. This will be modified in a future version of Flower to allow users to set `NodeConfig` for simulated `ClientApp`s. -## Run Flower with tensorflow-privacy and TensorFlow +### Install dependencies and project -### 1. Start the long-running Flower server (SuperLink) +Install the dependencies defined in `pyproject.toml` as well as the `tf_privacy` package. -```bash -flower-superlink --insecure +```shell +# From a new python environment, run: +pip install -e . ``` -### 2. Start the long-running Flower clients (SuperNodes) +## Run the project -Start 2 Flower `SuperNodes` in 2 separate terminal windows, using: +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. -```bash -flower-client-app client:appA --insecure -``` +### Run with the Simulation Engine ```bash -flower-client-app client:appB --insecure +flwr run . ``` -tensorflow-privacy hyperparameters can be passed for each client in `ClientApp` instantiation (in `client.py`). In this example, `noise_multiplier=1.5` and `noise_multiplier=1` are used for the first and second client respectively. - -### 3. 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 "l2-norm-clip=1.5 num-server-rounds=5" ``` diff --git a/examples/tensorflow-privacy/client.py b/examples/tensorflow-privacy/client.py deleted file mode 100644 index 85ed8a3d4245..000000000000 --- a/examples/tensorflow-privacy/client.py +++ /dev/null @@ -1,150 +0,0 @@ -import argparse -import os - -import tensorflow as tf -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, -) - -# Make TensorFlow log less verbose -os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" - - -def load_data(partition_id, batch_size): - fds = FederatedDataset(dataset="mnist", partitioners={"train": 2}) - 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"]["image"] / 255.0, partition["train"]["label"] - x_test, y_test = partition["test"]["image"] / 255.0, partition["test"]["label"] - - # Adjust the size of the training dataset to make it evenly divisible by the batch size - remainder = len(x_train) % batch_size - if remainder != 0: - x_train = x_train[:-remainder] - y_train = y_train[:-remainder] - - return (x_train, y_train), (x_test, y_test) - - -class FlowerClient(NumPyClient): - def __init__( - self, - model, - train_data, - test_data, - l2_norm_clip, - noise_multiplier, - num_microbatches, - learning_rate, - batch_size, - ) -> None: - super().__init__() - self.model = model - self.x_train, self.y_train = train_data - self.x_test, self.y_test = test_data - self.noise_multiplier = noise_multiplier - self.l2_norm_clip = l2_norm_clip - self.num_microbatches = num_microbatches - self.learning_rate = learning_rate - self.batch_size = batch_size - if self.batch_size % self.num_microbatches != 0: - raise ValueError( - f"Batch size {self.batch_size} is not divisible by the number of microbatches {self.num_microbatches}" - ) - - self.optimizer = tensorflow_privacy.DPKerasSGDOptimizer( - l2_norm_clip=l2_norm_clip, - noise_multiplier=noise_multiplier, - num_microbatches=num_microbatches, - learning_rate=learning_rate, - ) - loss = tf.keras.losses.SparseCategoricalCrossentropy( - reduction=tf.losses.Reduction.NONE - ) - self.model.compile(optimizer=self.optimizer, loss=loss, metrics=["accuracy"]) - - def get_parameters(self, config): - return self.model.get_weights() - - def fit(self, parameters, config): - self.model.set_weights(parameters) - - self.model.fit( - self.x_train, - self.y_train, - epochs=1, - batch_size=self.batch_size, - ) - - compute_dp_sgd_privacy_statement( - number_of_examples=self.x_train.shape[0], - batch_size=self.batch_size, - num_epochs=1, - noise_multiplier=self.noise_multiplier, - delta=1e-5, - ) - - return self.model.get_weights(), len(self.x_train), {} - - def evaluate(self, parameters, config): - self.model.set_weights(parameters) - self.model.compile( - optimizer=self.optimizer, - loss="sparse_categorical_crossentropy", - metrics=["accuracy"], - ) - loss, accuracy = self.model.evaluate(self.x_test, self.y_test) - return loss, len(self.x_test), {"accuracy": accuracy} - - -def client_fn_parameterized( - partition_id, - noise_multiplier, - l2_norm_clip=1.0, - num_microbatches=64, - learning_rate=0.01, - batch_size=64, -): - def client_fn(cid: str): - model = tf.keras.Sequential( - [ - tf.keras.layers.InputLayer(input_shape=(28, 28, 1)), - tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation="relu"), - tf.keras.layers.MaxPooling2D(pool_size=(2, 2)), - tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation="relu"), - tf.keras.layers.MaxPooling2D(pool_size=(2, 2)), - tf.keras.layers.Flatten(), - tf.keras.layers.Dense(128, activation="relu"), - tf.keras.layers.Dense(10, activation="softmax"), - ] - ) - train_data, test_data = load_data( - partition_id=partition_id, batch_size=batch_size - ) - return FlowerClient( - model, - train_data, - test_data, - noise_multiplier, - l2_norm_clip, - num_microbatches, - learning_rate, - batch_size, - ).to_client() - - return client_fn - - -appA = ClientApp( - client_fn=client_fn_parameterized(partition_id=0, noise_multiplier=1.0), -) - -appB = ClientApp( - client_fn=client_fn_parameterized(partition_id=1, noise_multiplier=1.5), -) diff --git a/examples/tensorflow-privacy/pyproject.toml b/examples/tensorflow-privacy/pyproject.toml index 884ba3b5f07b..48248cb31195 100644 --- a/examples/tensorflow-privacy/pyproject.toml +++ b/examples/tensorflow-privacy/pyproject.toml @@ -4,14 +4,11 @@ build-backend = "hatchling.build" [project] name = "tensorflow-privacy-fl" -version = "0.1.0" +version = "1.0.0" description = "Sample-level Differential Privacy with Tensorflow-Privacy in Flower" -authors = [ - { name = "The Flower Authors", email = "hello@flower.ai" }, -] dependencies = [ - "flwr>=1.8.0,<2.0", - "flwr-datasets[vision]>=0.1.0,<1.0.0", + "flwr[simulation]>=1.11.0", + "flwr-datasets[vision]>=0.3.0", "tensorflow-estimator~=2.4", "tensorflow-probability~=0.22.0", "tensorflow>=2.4.0,<=2.15.0", @@ -20,3 +17,23 @@ dependencies = [ [tool.hatch.build.targets.wheel] packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "tf_privacy.server_app:app" +clientapp = "tf_privacy.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 3 +l2-norm-clip = 1.0 +num-microbatches = 64 +learning-rate = 0.01 +batch-size = 64 + +[tool.flwr.federations] +default = "local-simulation" + +[tool.flwr.federations.local-simulation] +options.num-supernodes = 2 diff --git a/examples/tensorflow-privacy/server.py b/examples/tensorflow-privacy/server.py deleted file mode 100644 index 5b2ac6a3c4df..000000000000 --- a/examples/tensorflow-privacy/server.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import List, Tuple - -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: - 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(evaluate_metrics_aggregation_fn=weighted_average) - -config = ServerConfig(num_rounds=3) - -app = ServerApp( - config=config, - strategy=strategy, -) diff --git a/examples/tensorflow-privacy/tf_privacy/__init__.py b/examples/tensorflow-privacy/tf_privacy/__init__.py new file mode 100644 index 000000000000..252b33cdd1c5 --- /dev/null +++ b/examples/tensorflow-privacy/tf_privacy/__init__.py @@ -0,0 +1 @@ +"""tf_privacy: Training with Sample-Level Differential Privacy using TensorFlow-Privacy Engine.""" diff --git a/examples/tensorflow-privacy/tf_privacy/client_app.py b/examples/tensorflow-privacy/tf_privacy/client_app.py new file mode 100644 index 000000000000..977d98bbbe43 --- /dev/null +++ b/examples/tensorflow-privacy/tf_privacy/client_app.py @@ -0,0 +1,93 @@ +"""tf_privacy: Training with Sample-Level Differential Privacy using TensorFlow-Privacy Engine.""" + +import os + +import tensorflow as tf +import tensorflow_privacy +from flwr.client import ClientApp, NumPyClient +from tensorflow_privacy.privacy.analysis.compute_dp_sgd_privacy_lib import ( + compute_dp_sgd_privacy_statement, +) +from flwr.common import Context + +from tf_privacy.task import load_data, load_model +import numpy as np + + +# Make TensorFlow log less verbose +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" + + +class FlowerClient(NumPyClient): + def __init__( + self, + train_data, + test_data, + noise_multiplier, + run_config, + ) -> None: + super().__init__() + self.model = load_model() + self.x_train, self.y_train = train_data + self.x_train = np.expand_dims(self.x_train, axis=-1) + self.x_test, self.y_test = test_data + self.x_test = np.expand_dims(self.x_test, axis=-1) + self.noise_multiplier = noise_multiplier + self.run_config = run_config + if self.run_config["batch-size"] % self.run_config["num-microbatches"] != 0: + raise ValueError( + f"Batch size {self.run_config['batch-size']} is not divisible by the number of microbatches {self.run_config['num-microbatches']}" + ) + + self.optimizer = tensorflow_privacy.DPKerasSGDOptimizer( + l2_norm_clip=self.run_config["l2-norm-clip"], + noise_multiplier=self.noise_multiplier, + num_microbatches=self.run_config["num-microbatches"], + learning_rate=self.run_config["learning-rate"], + ) + loss = tf.keras.losses.SparseCategoricalCrossentropy( + reduction=tf.losses.Reduction.NONE + ) + self.model.compile(optimizer=self.optimizer, loss=loss, metrics=["accuracy"]) + + def fit(self, parameters, config): + self.model.set_weights(parameters) + self.model.fit( + self.x_train, + self.y_train, + epochs=1, + batch_size=self.run_config["batch-size"], + ) + + dp_statement = compute_dp_sgd_privacy_statement( + number_of_examples=self.x_train.shape[0], + batch_size=self.run_config["batch-size"], + num_epochs=1, + noise_multiplier=self.noise_multiplier, + delta=1e-5, + ) + print(dp_statement) + + 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) + return loss, len(self.x_test), {"accuracy": accuracy} + + +def client_fn(context: Context): + partition_id = context.node_config["partition-id"] + run_config = context.run_config + noise_multiplier = 1.0 if partition_id % 2 == 0 else 1.5 + + train_data, test_data = load_data( + partition_id=partition_id, + num_partitions=context.node_config["num-partitions"], + batch_size=context.run_config["batch-size"], + ) + + return FlowerClient(train_data, test_data, noise_multiplier, run_config).to_client() + + +app = ClientApp(client_fn=client_fn) diff --git a/examples/tensorflow-privacy/tf_privacy/server_app.py b/examples/tensorflow-privacy/tf_privacy/server_app.py new file mode 100644 index 000000000000..5348492a3ac4 --- /dev/null +++ b/examples/tensorflow-privacy/tf_privacy/server_app.py @@ -0,0 +1,31 @@ +"""tf_privacy: Training with Sample-Level Differential Privacy using TensorFlow-Privacy Engine.""" + +from typing import List, Tuple + +from flwr.common import Metrics +from flwr.server import ServerApp, ServerConfig, ServerAppComponents +from flwr.server.strategy import FedAvg +from flwr.common import Context, ndarrays_to_parameters +from .task import load_model + + +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> 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: + parameters = ndarrays_to_parameters(load_model().get_weights()) + strategy = FedAvg( + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) + 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/tensorflow-privacy/tf_privacy/task.py b/examples/tensorflow-privacy/tf_privacy/task.py new file mode 100644 index 000000000000..7bbf2a3e9c09 --- /dev/null +++ b/examples/tensorflow-privacy/tf_privacy/task.py @@ -0,0 +1,52 @@ +"""tf_privacy: Training with Sample-Level Differential Privacy using TensorFlow-Privacy Engine.""" + +import tensorflow as tf + +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner + +fds = None # Cache FederatedDataset + + +def load_model(): + model = tf.keras.Sequential( + [ + tf.keras.layers.InputLayer(input_shape=(28, 28, 1)), + tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation="relu"), + tf.keras.layers.MaxPooling2D(pool_size=(2, 2)), + tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation="relu"), + tf.keras.layers.MaxPooling2D(pool_size=(2, 2)), + tf.keras.layers.Flatten(), + tf.keras.layers.Dense(128, activation="relu"), + tf.keras.layers.Dense(10, activation="softmax"), + ] + ) + + return model + + +def load_data(partition_id: int, num_partitions: int, batch_size): + + 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) + 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"]["image"] / 255.0, partition["train"]["label"] + x_test, y_test = partition["test"]["image"] / 255.0, partition["test"]["label"] + + # Adjust the size of the training dataset to make it evenly divisible by the batch size + remainder = len(x_train) % batch_size + if remainder != 0: + x_train = x_train[:-remainder] + y_train = y_train[:-remainder] + + return (x_train, y_train), (x_test, y_test) From e8494058e5555253bac28071bbbf7938e926ee1e Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Tue, 17 Sep 2024 16:01:33 +0100 Subject: [PATCH 02/16] refactor(examples:skip) Update quickstart-tensorflow example (#4227) --- examples/quickstart-tensorflow/tfexample/client_app.py | 4 ---- examples/quickstart-tensorflow/tfexample/server_app.py | 1 - 2 files changed, 5 deletions(-) diff --git a/examples/quickstart-tensorflow/tfexample/client_app.py b/examples/quickstart-tensorflow/tfexample/client_app.py index 05bf15e074c2..fcea79ba7391 100644 --- a/examples/quickstart-tensorflow/tfexample/client_app.py +++ b/examples/quickstart-tensorflow/tfexample/client_app.py @@ -21,10 +21,6 @@ def __init__( 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) diff --git a/examples/quickstart-tensorflow/tfexample/server_app.py b/examples/quickstart-tensorflow/tfexample/server_app.py index 053e92588e67..a09ceccfb3f2 100644 --- a/examples/quickstart-tensorflow/tfexample/server_app.py +++ b/examples/quickstart-tensorflow/tfexample/server_app.py @@ -22,7 +22,6 @@ 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 From 2e8ca6c815d3436d4dc46fdd0968f9027a1b1661 Mon Sep 17 00:00:00 2001 From: Ray Sun Date: Tue, 17 Sep 2024 17:00:44 +0100 Subject: [PATCH 03/16] docs(framework) Add new blockchain example to FAQ (#3993) --- doc/source/ref-faq.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/ref-faq.rst b/doc/source/ref-faq.rst index 26b7dca4a0a7..e3bd754d481c 100644 --- a/doc/source/ref-faq.rst +++ b/doc/source/ref-faq.rst @@ -25,6 +25,9 @@ This page collects answers to commonly asked questions about Federated Learning Yes, of course. A list of available examples using Flower within a blockchain environment is available here: + * `FLock: A Decentralised AI Training Platform `_. + * Contribute to on-chain training the model and earn rewards. + * Local blockchain with federated learning simulation. * `Flower meets Nevermined GitHub Repository `_. * `Flower meets Nevermined YouTube video `_. * `Flower meets KOSMoS `_. From 33710c5fb7e43690a1cadff067b15666c76cef8c Mon Sep 17 00:00:00 2001 From: Yan Gao Date: Wed, 18 Sep 2024 07:57:25 +0100 Subject: [PATCH 04/16] refactor(benchmarks) Update llm leaderboard Readmes (#4228) --- benchmarks/flowertune-llm/README.md | 21 ++++++++++--------- .../flowertune-llm/evaluation/README.md | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/benchmarks/flowertune-llm/README.md b/benchmarks/flowertune-llm/README.md index ed2f8821cd88..f45b7a6198b7 100644 --- a/benchmarks/flowertune-llm/README.md +++ b/benchmarks/flowertune-llm/README.md @@ -1,4 +1,4 @@ -![](_static/flower_llm.png) +[![FlowerTune LLM Leaderboard](_static/flower_llm.png)](https://flower.ai/benchmarks/llm-leaderboard) # FlowerTune LLM Leaderboard @@ -27,15 +27,16 @@ flwr new --framework=FlowerTune The `flwr new` command will generate a directory with the following structure: ```bash - - ├── README.md # <- Instructions - ├── pyproject.toml # <- Environment dependencies and configs - └── - ├── client_app.py # <- Flower ClientApp build - ├── dataset.py # <- Dataset and tokenizer build - ├── models.py # <- Model build - ├── server_app.py # <- Flower ServerApp build - └── strategy.py # <- Flower strategy build + +├── README.md # Instructions +├── pyproject.toml # Environment dependencies and configs +└── + ├── __init__.py + ├── client_app.py # Flower ClientApp build + ├── dataset.py # Dataset and tokenizer build + ├── 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. diff --git a/benchmarks/flowertune-llm/evaluation/README.md b/benchmarks/flowertune-llm/evaluation/README.md index d7216c089d8a..e2a7477fca76 100644 --- a/benchmarks/flowertune-llm/evaluation/README.md +++ b/benchmarks/flowertune-llm/evaluation/README.md @@ -5,7 +5,7 @@ If you are participating [LLM Leaderboard](https://flower.ai/benchmarks/llm-lead ## 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. +Navigate to the directory corresponding to your selected challenge ([`general NLP`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation/general-nlp), [`finance`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation/finance), [`medical`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation/medical), or [`code`](https://github.com/adap/flower/tree/main/benchmarks/flowertune-llm/evaluation/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. From 1d02874f78c61ac07ee082c752054581953c8907 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Wed, 18 Sep 2024 16:33:49 +0100 Subject: [PATCH 05/16] refactor(framework) Migrate ID handling from sint64 to uint64 (#4170) Co-authored-by: Heng Pan Co-authored-by: Daniel J. Beutel --- src/proto/flwr/proto/clientappio.proto | 6 +- src/proto/flwr/proto/driver.proto | 4 +- src/proto/flwr/proto/exec.proto | 4 +- src/proto/flwr/proto/message.proto | 8 +- src/proto/flwr/proto/node.proto | 2 +- src/proto/flwr/proto/run.proto | 4 +- src/proto/flwr/proto/task.proto | 4 +- src/py/flwr/proto/clientappio_pb2.py | 2 +- src/py/flwr/proto/driver_pb2.py | 2 +- src/py/flwr/proto/exec_pb2.py | 2 +- src/py/flwr/proto/message_pb2.py | 2 +- src/py/flwr/proto/node_pb2.py | 2 +- src/py/flwr/proto/run_pb2.py | 2 +- src/py/flwr/proto/task_pb2.py | 2 +- .../server/superlink/state/sqlite_state.py | 118 +++++++++++++++--- src/py/flwr/server/superlink/state/utils.py | 100 ++++++++++++++- .../flwr/server/superlink/state/utils_test.py | 92 ++++++++++++++ 17 files changed, 312 insertions(+), 44 deletions(-) create mode 100644 src/py/flwr/server/superlink/state/utils_test.py diff --git a/src/proto/flwr/proto/clientappio.proto b/src/proto/flwr/proto/clientappio.proto index 0ec73b8e569a..19d2db50501a 100644 --- a/src/proto/flwr/proto/clientappio.proto +++ b/src/proto/flwr/proto/clientappio.proto @@ -45,9 +45,9 @@ message ClientAppOutputStatus { } message GetTokenRequest {} -message GetTokenResponse { sint64 token = 1; } +message GetTokenResponse { uint64 token = 1; } -message PullClientAppInputsRequest { sint64 token = 1; } +message PullClientAppInputsRequest { uint64 token = 1; } message PullClientAppInputsResponse { Message message = 1; Context context = 2; @@ -56,7 +56,7 @@ message PullClientAppInputsResponse { } message PushClientAppOutputsRequest { - sint64 token = 1; + uint64 token = 1; Message message = 2; Context context = 3; } diff --git a/src/proto/flwr/proto/driver.proto b/src/proto/flwr/proto/driver.proto index 63a2f78e6f6d..c7ae7dcf30f0 100644 --- a/src/proto/flwr/proto/driver.proto +++ b/src/proto/flwr/proto/driver.proto @@ -50,10 +50,10 @@ message CreateRunRequest { map override_config = 3; Fab fab = 4; } -message CreateRunResponse { sint64 run_id = 1; } +message CreateRunResponse { uint64 run_id = 1; } // GetNodes messages -message GetNodesRequest { sint64 run_id = 1; } +message GetNodesRequest { uint64 run_id = 1; } message GetNodesResponse { repeated Node nodes = 1; } // PushTaskIns messages diff --git a/src/proto/flwr/proto/exec.proto b/src/proto/flwr/proto/exec.proto index 65faf4386ea0..ad0723c0480c 100644 --- a/src/proto/flwr/proto/exec.proto +++ b/src/proto/flwr/proto/exec.proto @@ -33,6 +33,6 @@ message StartRunRequest { map override_config = 2; map federation_config = 3; } -message StartRunResponse { sint64 run_id = 1; } -message StreamLogsRequest { sint64 run_id = 1; } +message StartRunResponse { uint64 run_id = 1; } +message StreamLogsRequest { uint64 run_id = 1; } message StreamLogsResponse { string log_output = 1; } diff --git a/src/proto/flwr/proto/message.proto b/src/proto/flwr/proto/message.proto index 3230ab0609a9..7066da5b7e76 100644 --- a/src/proto/flwr/proto/message.proto +++ b/src/proto/flwr/proto/message.proto @@ -28,17 +28,17 @@ message Message { } message Context { - sint64 node_id = 1; + uint64 node_id = 1; map node_config = 2; RecordSet state = 3; map run_config = 4; } message Metadata { - sint64 run_id = 1; + uint64 run_id = 1; string message_id = 2; - sint64 src_node_id = 3; - sint64 dst_node_id = 4; + uint64 src_node_id = 3; + uint64 dst_node_id = 4; string reply_to_message = 5; string group_id = 6; double ttl = 7; diff --git a/src/proto/flwr/proto/node.proto b/src/proto/flwr/proto/node.proto index e61d44f0f783..ec72b51b44ec 100644 --- a/src/proto/flwr/proto/node.proto +++ b/src/proto/flwr/proto/node.proto @@ -18,6 +18,6 @@ syntax = "proto3"; package flwr.proto; message Node { - sint64 node_id = 1; + uint64 node_id = 1; bool anonymous = 2; } diff --git a/src/proto/flwr/proto/run.proto b/src/proto/flwr/proto/run.proto index 6adca5c2437b..fc3294f7a583 100644 --- a/src/proto/flwr/proto/run.proto +++ b/src/proto/flwr/proto/run.proto @@ -20,11 +20,11 @@ package flwr.proto; import "flwr/proto/transport.proto"; message Run { - sint64 run_id = 1; + uint64 run_id = 1; string fab_id = 2; string fab_version = 3; map override_config = 4; string fab_hash = 5; } -message GetRunRequest { sint64 run_id = 1; } +message GetRunRequest { uint64 run_id = 1; } message GetRunResponse { Run run = 1; } diff --git a/src/proto/flwr/proto/task.proto b/src/proto/flwr/proto/task.proto index 936b8120e495..324a70a5359c 100644 --- a/src/proto/flwr/proto/task.proto +++ b/src/proto/flwr/proto/task.proto @@ -37,13 +37,13 @@ message Task { message TaskIns { string task_id = 1; string group_id = 2; - sint64 run_id = 3; + uint64 run_id = 3; Task task = 4; } message TaskRes { string task_id = 1; string group_id = 2; - sint64 run_id = 3; + uint64 run_id = 3; Task task = 4; } diff --git a/src/py/flwr/proto/clientappio_pb2.py b/src/py/flwr/proto/clientappio_pb2.py index 9fd5302fe6cd..3fdc9f8a6ece 100644 --- a/src/py/flwr/proto/clientappio_pb2.py +++ b/src/py/flwr/proto/clientappio_pb2.py @@ -17,7 +17,7 @@ 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\"\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') +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(\x04\"+\n\x1aPullClientAppInputsRequest\x12\r\n\x05token\x18\x01 \x01(\x04\"\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(\x04\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) diff --git a/src/py/flwr/proto/driver_pb2.py b/src/py/flwr/proto/driver_pb2.py index dde72620f5bf..1322660cfac1 100644 --- a/src/py/flwr/proto/driver_pb2.py +++ b/src/py/flwr/proto/driver_pb2.py @@ -19,7 +19,7 @@ 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\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') +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(\x04\"!\n\x0fGetNodesRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"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) diff --git a/src/py/flwr/proto/exec_pb2.py b/src/py/flwr/proto/exec_pb2.py index 3fe109067296..574f39eaa18d 100644 --- a/src/py/flwr/proto/exec_pb2.py +++ b/src/py/flwr/proto/exec_pb2.py @@ -16,7 +16,7 @@ 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\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') +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(\x04\"#\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"(\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) diff --git a/src/py/flwr/proto/message_pb2.py b/src/py/flwr/proto/message_pb2.py index 7e2555972a8a..d2201cb07b56 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\"\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') +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(\x04\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(\x04\x12\x12\n\nmessage_id\x18\x02 \x01(\t\x12\x13\n\x0bsrc_node_id\x18\x03 \x01(\x04\x12\x13\n\x0b\x64st_node_id\x18\x04 \x01(\x04\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) diff --git a/src/py/flwr/proto/node_pb2.py b/src/py/flwr/proto/node_pb2.py index b300f2c562c2..f94691db6c3f 100644 --- a/src/py/flwr/proto/node_pb2.py +++ b/src/py/flwr/proto/node_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"*\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x12\x12\x11\n\tanonymous\x18\x02 \x01(\x08\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"*\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\tanonymous\x18\x02 \x01(\x08\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) diff --git a/src/py/flwr/proto/run_pb2.py b/src/py/flwr/proto/run_pb2.py index 4892091a6a46..13fc43f90f8c 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\"\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') +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(\x04\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(\x04\".\n\x0eGetRunResponse\x12\x1c\n\x03run\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Runb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) diff --git a/src/py/flwr/proto/task_pb2.py b/src/py/flwr/proto/task_pb2.py index 3e044f9ec846..75b022dc65ea 100644 --- a/src/py/flwr/proto/task_pb2.py +++ b/src/py/flwr/proto/task_pb2.py @@ -17,7 +17,7 @@ from flwr.proto import error_pb2 as flwr_dot_proto_dot_error__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/task.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x16\x66lwr/proto/error.proto\"\x89\x02\n\x04Task\x12\"\n\x08producer\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\"\n\x08\x63onsumer\x18\x02 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x12\n\ncreated_at\x18\x03 \x01(\x01\x12\x14\n\x0c\x64\x65livered_at\x18\x04 \x01(\t\x12\x11\n\tpushed_at\x18\x05 \x01(\x01\x12\x0b\n\x03ttl\x18\x06 \x01(\x01\x12\x10\n\x08\x61ncestry\x18\x07 \x03(\t\x12\x11\n\ttask_type\x18\x08 \x01(\t\x12(\n\trecordset\x18\t \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12 \n\x05\x65rror\x18\n \x01(\x0b\x32\x11.flwr.proto.Error\"\\\n\x07TaskIns\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\x12\x12\x1e\n\x04task\x18\x04 \x01(\x0b\x32\x10.flwr.proto.Task\"\\\n\x07TaskRes\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\x12\x12\x1e\n\x04task\x18\x04 \x01(\x0b\x32\x10.flwr.proto.Taskb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/task.proto\x12\nflwr.proto\x1a\x15\x66lwr/proto/node.proto\x1a\x1a\x66lwr/proto/recordset.proto\x1a\x16\x66lwr/proto/error.proto\"\x89\x02\n\x04Task\x12\"\n\x08producer\x18\x01 \x01(\x0b\x32\x10.flwr.proto.Node\x12\"\n\x08\x63onsumer\x18\x02 \x01(\x0b\x32\x10.flwr.proto.Node\x12\x12\n\ncreated_at\x18\x03 \x01(\x01\x12\x14\n\x0c\x64\x65livered_at\x18\x04 \x01(\t\x12\x11\n\tpushed_at\x18\x05 \x01(\x01\x12\x0b\n\x03ttl\x18\x06 \x01(\x01\x12\x10\n\x08\x61ncestry\x18\x07 \x03(\t\x12\x11\n\ttask_type\x18\x08 \x01(\t\x12(\n\trecordset\x18\t \x01(\x0b\x32\x15.flwr.proto.RecordSet\x12 \n\x05\x65rror\x18\n \x01(\x0b\x32\x11.flwr.proto.Error\"\\\n\x07TaskIns\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\x04\x12\x1e\n\x04task\x18\x04 \x01(\x0b\x32\x10.flwr.proto.Task\"\\\n\x07TaskRes\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x10\n\x08group_id\x18\x02 \x01(\t\x12\x0e\n\x06run_id\x18\x03 \x01(\x04\x12\x1e\n\x04task\x18\x04 \x01(\x0b\x32\x10.flwr.proto.Taskb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) diff --git a/src/py/flwr/server/superlink/state/sqlite_state.py b/src/py/flwr/server/superlink/state/sqlite_state.py index 4bb31fa6cea5..286ab881f891 100644 --- a/src/py/flwr/server/superlink/state/sqlite_state.py +++ b/src/py/flwr/server/superlink/state/sqlite_state.py @@ -33,7 +33,14 @@ from flwr.server.utils.validator import validate_task_ins_or_res from .state import State -from .utils import generate_rand_int_from_bytes, make_node_unavailable_taskres +from .utils import ( + convert_sint64_to_uint64, + convert_sint64_values_in_dict_to_uint64, + convert_uint64_to_sint64, + convert_uint64_values_in_dict_to_sint64, + generate_rand_int_from_bytes, + make_node_unavailable_taskres, +) SQL_CREATE_TABLE_NODE = """ CREATE TABLE IF NOT EXISTS node( @@ -223,6 +230,12 @@ def store_task_ins(self, task_ins: TaskIns) -> Optional[UUID]: # Store TaskIns task_ins.task_id = str(task_id) data = (task_ins_to_dict(task_ins),) + + # Convert values from uint64 to sint64 for SQLite + convert_uint64_values_in_dict_to_sint64( + data[0], ["run_id", "producer_node_id", "consumer_node_id"] + ) + columns = ", ".join([f":{key}" for key in data[0]]) query = f"INSERT INTO task_ins VALUES({columns});" @@ -284,6 +297,9 @@ def get_task_ins( AND delivered_at = "" """ else: + # Convert the uint64 value to sint64 for SQLite + data["node_id"] = convert_uint64_to_sint64(node_id) + # Retrieve all TaskIns for node_id query = """ SELECT task_id @@ -292,7 +308,6 @@ def get_task_ins( AND consumer_node_id == :node_id AND delivered_at = "" """ - data["node_id"] = node_id if limit is not None: query += " LIMIT :limit" @@ -322,6 +337,12 @@ def get_task_ins( # Run query rows = self.query(query, data) + for row in rows: + # Convert values from sint64 to uint64 + convert_sint64_values_in_dict_to_uint64( + row, ["run_id", "producer_node_id", "consumer_node_id"] + ) + result = [dict_to_task_ins(row) for row in rows] return result @@ -354,6 +375,12 @@ def store_task_res(self, task_res: TaskRes) -> Optional[UUID]: # Store TaskIns task_res.task_id = str(task_id) data = (task_res_to_dict(task_res),) + + # Convert values from uint64 to sint64 for SQLite + convert_uint64_values_in_dict_to_sint64( + data[0], ["run_id", "producer_node_id", "consumer_node_id"] + ) + columns = ", ".join([f":{key}" for key in data[0]]) query = f"INSERT INTO task_res VALUES({columns});" @@ -431,6 +458,12 @@ def get_task_res(self, task_ids: set[UUID], limit: Optional[int]) -> list[TaskRe # Run query rows = self.query(query, data) + for row in rows: + # Convert values from sint64 to uint64 + convert_sint64_values_in_dict_to_uint64( + row, ["run_id", "producer_node_id", "consumer_node_id"] + ) + result = [dict_to_task_res(row) for row in rows] # 1. Query: Fetch consumer_node_id of remaining task_ids @@ -474,6 +507,13 @@ def get_task_res(self, task_ids: set[UUID], limit: Optional[int]) -> list[TaskRe for row in task_ins_rows: if limit and len(result) == limit: break + + for row in rows: + # Convert values from sint64 to uint64 + convert_sint64_values_in_dict_to_uint64( + row, ["run_id", "producer_node_id", "consumer_node_id"] + ) + task_ins = dict_to_task_ins(row) err_taskres = make_node_unavailable_taskres( ref_taskins=task_ins, @@ -544,8 +584,11 @@ def create_node( self, ping_interval: float, public_key: Optional[bytes] = None ) -> int: """Create, store in state, and return `node_id`.""" - # Sample a random int64 as node_id - node_id = generate_rand_int_from_bytes(NODE_ID_NUM_BYTES) + # Sample a random uint64 as node_id + uint64_node_id = generate_rand_int_from_bytes(NODE_ID_NUM_BYTES) + + # Convert the uint64 value to sint64 for SQLite + sint64_node_id = convert_uint64_to_sint64(uint64_node_id) query = "SELECT node_id FROM node WHERE public_key = :public_key;" row = self.query(query, {"public_key": public_key}) @@ -562,17 +605,28 @@ def create_node( try: self.query( - query, (node_id, time.time() + ping_interval, ping_interval, public_key) + query, + ( + sint64_node_id, + time.time() + ping_interval, + ping_interval, + public_key, + ), ) except sqlite3.IntegrityError: log(ERROR, "Unexpected node registration failure.") return 0 - return node_id + + # Note: we need to return the uint64 value of the node_id + return uint64_node_id def delete_node(self, node_id: int, public_key: Optional[bytes] = None) -> None: """Delete a node.""" + # Convert the uint64 value to sint64 for SQLite + sint64_node_id = convert_uint64_to_sint64(node_id) + query = "DELETE FROM node WHERE node_id = ?" - params = (node_id,) + params = (sint64_node_id,) if public_key is not None: query += " AND public_key = ?" @@ -597,15 +651,20 @@ def get_nodes(self, run_id: int) -> set[int]: If the provided `run_id` does not exist or has no matching nodes, an empty `Set` MUST be returned. """ + # Convert the uint64 value to sint64 for SQLite + sint64_run_id = convert_uint64_to_sint64(run_id) + # Validate run ID query = "SELECT COUNT(*) FROM run WHERE run_id = ?;" - if self.query(query, (run_id,))[0]["COUNT(*)"] == 0: + if self.query(query, (sint64_run_id,))[0]["COUNT(*)"] == 0: return set() # Get nodes query = "SELECT node_id FROM node WHERE online_until > ?;" rows = self.query(query, (time.time(),)) - result: set[int] = {row["node_id"] for row in rows} + + # Convert sint64 node_ids to uint64 + result: set[int] = {convert_sint64_to_uint64(row["node_id"]) for row in rows} return result def get_node_id(self, node_public_key: bytes) -> Optional[int]: @@ -614,7 +673,11 @@ def get_node_id(self, node_public_key: bytes) -> Optional[int]: row = self.query(query, {"public_key": node_public_key}) if len(row) > 0: node_id: int = row[0]["node_id"] - return node_id + + # Convert the sint64 value to uint64 after reading from SQLite + uint64_node_id = convert_sint64_to_uint64(node_id) + + return uint64_node_id return None def create_run( @@ -626,12 +689,15 @@ def create_run( ) -> int: """Create a new run for the specified `fab_id` and `fab_version`.""" # Sample a random int64 as run_id - run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) + uint64_run_id = generate_rand_int_from_bytes(RUN_ID_NUM_BYTES) + + # Convert the uint64 value to sint64 for SQLite + sint64_run_id = convert_uint64_to_sint64(uint64_run_id) # Check conflicts query = "SELECT COUNT(*) FROM run WHERE run_id = ?;" - # If run_id does not exist - if self.query(query, (run_id,))[0]["COUNT(*)"] == 0: + # If sint64_run_id does not exist + if self.query(query, (sint64_run_id,))[0]["COUNT(*)"] == 0: query = ( "INSERT INTO run " "(run_id, fab_id, fab_version, fab_hash, override_config)" @@ -639,14 +705,22 @@ def create_run( ) if fab_hash: self.query( - query, (run_id, "", "", fab_hash, json.dumps(override_config)) + query, + (sint64_run_id, "", "", fab_hash, json.dumps(override_config)), ) else: self.query( query, - (run_id, fab_id, fab_version, "", json.dumps(override_config)), + ( + sint64_run_id, + fab_id, + fab_version, + "", + json.dumps(override_config), + ), ) - return run_id + # Note: we need to return the uint64 value of the run_id + return uint64_run_id log(ERROR, "Unexpected run creation failure.") return 0 @@ -705,11 +779,13 @@ def get_node_public_keys(self) -> set[bytes]: def get_run(self, run_id: int) -> Optional[Run]: """Retrieve information about the run with the specified `run_id`.""" + # Convert the uint64 value to sint64 for SQLite + sint64_run_id = convert_uint64_to_sint64(run_id) query = "SELECT * FROM run WHERE run_id = ?;" try: - row = self.query(query, (run_id,))[0] + row = self.query(query, (sint64_run_id,))[0] return Run( - run_id=run_id, + run_id=convert_sint64_to_uint64(row["run_id"]), fab_id=row["fab_id"], fab_version=row["fab_version"], fab_hash=row["fab_hash"], @@ -721,10 +797,14 @@ def get_run(self, run_id: int) -> Optional[Run]: def acknowledge_ping(self, node_id: int, ping_interval: float) -> bool: """Acknowledge a ping received from a node, serving as a heartbeat.""" + sint64_node_id = convert_uint64_to_sint64(node_id) + # Update `online_until` and `ping_interval` for the given `node_id` query = "UPDATE node SET online_until = ?, ping_interval = ? WHERE node_id = ?;" try: - self.query(query, (time.time() + ping_interval, ping_interval, node_id)) + self.query( + query, (time.time() + ping_interval, ping_interval, sint64_node_id) + ) return True except sqlite3.IntegrityError: log(ERROR, "`node_id` does not exist.") diff --git a/src/py/flwr/server/superlink/state/utils.py b/src/py/flwr/server/superlink/state/utils.py index b12a87ac998d..00ba02d2e43b 100644 --- a/src/py/flwr/server/superlink/state/utils.py +++ b/src/py/flwr/server/superlink/state/utils.py @@ -33,8 +33,104 @@ def generate_rand_int_from_bytes(num_bytes: int) -> int: - """Generate a random `num_bytes` integer.""" - return int.from_bytes(urandom(num_bytes), "little", signed=True) + """Generate a random unsigned integer from `num_bytes` bytes.""" + return int.from_bytes(urandom(num_bytes), "little", signed=False) + + +def convert_uint64_to_sint64(u: int) -> int: + """Convert a uint64 value to a sint64 value with the same bit sequence. + + Parameters + ---------- + u : int + The unsigned 64-bit integer to convert. + + Returns + ------- + int + The signed 64-bit integer equivalent. + + The signed 64-bit integer will have the same bit pattern as the + unsigned 64-bit integer but may have a different decimal value. + + For numbers within the range [0, `sint64` max value], the decimal + value remains the same. However, for numbers greater than the `sint64` + max value, the decimal value will differ due to the wraparound caused + by the sign bit. + """ + if u >= (1 << 63): + return u - (1 << 64) + return u + + +def convert_sint64_to_uint64(s: int) -> int: + """Convert a sint64 value to a uint64 value with the same bit sequence. + + Parameters + ---------- + s : int + The signed 64-bit integer to convert. + + Returns + ------- + int + The unsigned 64-bit integer equivalent. + + The unsigned 64-bit integer will have the same bit pattern as the + signed 64-bit integer but may have a different decimal value. + + For negative `sint64` values, the conversion adds 2^64 to the + signed value to obtain the equivalent `uint64` value. For non-negative + `sint64` values, the decimal value remains unchanged in the `uint64` + representation. + """ + if s < 0: + return s + (1 << 64) + return s + + +def convert_uint64_values_in_dict_to_sint64( + data_dict: dict[str, int], keys: list[str] +) -> None: + """Convert uint64 values to sint64 in the given dictionary. + + Parameters + ---------- + data_dict : dict[str, int] + A dictionary where the values are integers to be converted. + keys : list[str] + A list of keys in the dictionary whose values need to be converted. + + Returns + ------- + None + This function does not return a value. It modifies `data_dict` in place. + """ + for key in keys: + if key in data_dict: + data_dict[key] = convert_uint64_to_sint64(data_dict[key]) + + +def convert_sint64_values_in_dict_to_uint64( + data_dict: dict[str, int], keys: list[str] +) -> None: + """Convert sint64 values to uint64 in the given dictionary. + + Parameters + ---------- + data_dict : dict[str, int] + A dictionary where the values are integers to be converted. + keys : list[str] + A list of keys in the dictionary whose values need to be converted. + + Returns + ------- + None + This function does not return a value. It modifies `data_dict` in place. + """ + for key in keys: + if key in data_dict: + data_dict[key] = convert_sint64_to_uint64(data_dict[key]) def make_node_unavailable_taskres(ref_taskins: TaskIns) -> TaskRes: diff --git a/src/py/flwr/server/superlink/state/utils_test.py b/src/py/flwr/server/superlink/state/utils_test.py new file mode 100644 index 000000000000..93e678ddd38a --- /dev/null +++ b/src/py/flwr/server/superlink/state/utils_test.py @@ -0,0 +1,92 @@ +# 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. +# ============================================================================== +"""Utils tests.""" + +import unittest + +from parameterized import parameterized + +from .utils import ( + convert_sint64_to_uint64, + convert_uint64_to_sint64, + generate_rand_int_from_bytes, +) + + +class UtilsTest(unittest.TestCase): + """Test utils code.""" + + @parameterized.expand( # type: ignore + [ + # Test values within the positive range of sint64 (below 2^63) + (0, 0), # Minimum positive value + (1, 1), # 1 remains 1 in both uint64 and sint64 + (2**62, 2**62), # Mid-range positive value + (2**63 - 1, 2**63 - 1), # Maximum positive value for sint64 + # Test values at or above 2^63 (become negative in sint64) + (2**63, -(2**63)), # Minimum negative value for sint64 + (2**63 + 1, -(2**63) + 1), # Slightly above the boundary + (9223372036854775811, -9223372036854775805), # Some value > sint64 max + (2**64 - 1, -1), # Maximum uint64 value becomes -1 in sint64 + ] + ) + def test_convert_uint64_to_sint64(self, before: int, after: int) -> None: + """Test conversion from uint64 to sint64.""" + self.assertEqual(convert_uint64_to_sint64(before), after) + + @parameterized.expand( # type: ignore + [ + # Test values within the negative range of sint64 + (-(2**63), 2**63), # Minimum sint64 value becomes 2^63 in uint64 + (-(2**63) + 1, 2**63 + 1), # Slightly above the minimum + (-9223372036854775805, 9223372036854775811), # Some value > sint64 max + # Test zero-adjacent inputs + (-1, 2**64 - 1), # -1 in sint64 becomes 2^64 - 1 in uint64 + (0, 0), # 0 remains 0 in both sint64 and uint64 + (1, 1), # 1 remains 1 in both sint64 and uint64 + # Test values within the positive range of sint64 + (2**63 - 1, 2**63 - 1), # Maximum positive value in sint64 + # Test boundary and maximum uint64 value + (2**63, 2**63), # Exact boundary value for sint64 + (2**64 - 1, 2**64 - 1), # Maximum uint64 value, stays the same + ] + ) + def test_sint64_to_uint64(self, before: int, after: int) -> None: + """Test conversion from sint64 to uint64.""" + self.assertEqual(convert_sint64_to_uint64(before), after) + + @parameterized.expand( # type: ignore + [ + (0), + (1), + (2**62), + (2**63 - 1), + (2**63), + (2**63 + 1), + (9223372036854775811), + (2**64 - 1), + ] + ) + def test_uint64_to_sint64_to_uint64(self, expected: int) -> None: + """Test conversion from sint64 to uint64.""" + actual = convert_sint64_to_uint64(convert_uint64_to_sint64(expected)) + self.assertEqual(expected, actual) + + def test_generate_rand_int_from_bytes_unsigned_int(self) -> None: + """Test that the generated integer is unsigned (non-negative).""" + for num_bytes in range(1, 9): + with self.subTest(num_bytes=num_bytes): + rand_int = generate_rand_int_from_bytes(num_bytes) + self.assertGreaterEqual(rand_int, 0) From 89618b04ca6ac84303099014f8ee67697bdb9544 Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Wed, 18 Sep 2024 18:25:20 +0100 Subject: [PATCH 06/16] docs(framework) Update Quickstart Tutorial documentation for TensorFlow with `flwr run` (#3338) Co-authored-by: Javier Co-authored-by: Charles Beauville --- doc/source/tutorial-quickstart-tensorflow.rst | 452 ++++++++++++------ 1 file changed, 294 insertions(+), 158 deletions(-) diff --git a/doc/source/tutorial-quickstart-tensorflow.rst b/doc/source/tutorial-quickstart-tensorflow.rst index bd63eb461d21..ffcd9efeb9bc 100644 --- a/doc/source/tutorial-quickstart-tensorflow.rst +++ b/doc/source/tutorial-quickstart-tensorflow.rst @@ -1,171 +1,307 @@ .. _quickstart-tensorflow: +####################### + Quickstart TensorFlow +####################### + +In this tutorial we will learn how to train a Convolutional Neural +Network on CIFAR-10 using the Flower framework and TensorFlow. 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+TensorFlow 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:: shell + + # 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 ``TensorFlow``), give a name to your +project, and type in your developer name: + +.. code:: shell + + $ 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:: 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:: shell + + # From the directory where your pyproject.toml is + $ pip install -e . + +To run the project, do: + +.. code:: shell + + # Run with default arguments + $ flwr run . + +With default arguments you will see an output like this one: + +.. code:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Starting evaluation of initial global parameters + INFO : Evaluation returned no results (`None`) + 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 31.31s + INFO : History (loss, distributed): + INFO : round 1: 1.9066195368766785 + INFO : round 2: 1.657227087020874 + INFO : round 3: 1.559039831161499 + 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 batch-size=16" + +********** + 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 the ``NumPy`` arrays 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, "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"] + +*********** + The Model +*********** + +Next, we need a 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 + + 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"), + ] + ) + model.compile( + "adam", + loss="sparse_categorical_crossentropy", + metrics=["accuracy"], + ) + return model + +*************** + The ClientApp +*************** + +With `TensorFlow`, we can use the built-in ``get_weights()`` and +``set_weights()`` functions, which simplifies the implementation with +`Flower`. The rest of the functionality in the ClientApp 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, model, data, epochs, batch_size, verbose): + self.model = model + 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 fit(self, parameters, config): + 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): + 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} + +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 hyperparameters defined in your +``pyproject.toml`` to configure the run. For example, in this tutorial +we access the `local-epochs` setting to control the number of epochs a +``ClientApp`` will perform when running the ``fit()`` method, in +addition to `batch-size`. You could define additional hyperparameters in +``pyproject.toml`` and access them here. + +.. code:: python + + def client_fn(context: Context): + # Load model and data + net = load_model() + + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["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, data, epochs, batch_size, verbose + ).to_client() + + + # Flower ClientApp + app = ClientApp(client_fn=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 serve as the global model to federate. + +.. code:: python + + 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, + 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) -Quickstart TensorFlow -===================== - -.. meta:: - :description: Check out this Federated Learning quickstart tutorial for using Flower with TensorFlow to train a MobilNetV2 model on CIFAR-10. - -.. youtube:: FGTc2TQq7VM - :width: 100% - -Let's build a federated learning system in less than 20 lines of code! - -Before Flower can be imported we have to install it: - -.. code-block:: shell - - $ pip install flwr - -Since we want to use the Keras API of TensorFlow (TF), we have to install TF as well: - -.. code-block:: shell - - $ pip install tensorflow - - -Flower Client -------------- - -Next, in a file called :code:`client.py`, import Flower and TensorFlow: - -.. code-block:: python - - import flwr as fl - import tensorflow as tf - -We use the Keras utilities of TF to load CIFAR10, a popular colored image classification -dataset for machine learning. The call to -:code:`tf.keras.datasets.cifar10.load_data()` downloads CIFAR10, caches it locally, -and then returns the entire training and test set as NumPy ndarrays. - -.. code-block:: python - - (x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data() - -Next, we need a model. For the purpose of this tutorial, we use MobilNetV2 with 10 output classes: - -.. code-block:: python - - model = tf.keras.applications.MobileNetV2((32, 32, 3), classes=10, weights=None) - model.compile("adam", "sparse_categorical_crossentropy", metrics=["accuracy"]) - -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 Keras. -The :code:`NumPyClient` interface defines three methods which can be -implemented in the following way: - -.. code-block:: python - - class CifarClient(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, steps_per_epoch=3) - 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": float(accuracy)} - - -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()) - - -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. - - -Flower Server -------------- - -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: - -.. code-block:: python - - import flwr as fl - - fl.server.start_server(config=fl.server.ServerConfig(num_rounds=3)) - - -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: - -.. code-block:: shell - - $ python server.py - -Once the server is running we can start the clients in different terminals. -Open a new terminal and start the first client: +Congratulations! You've successfully built and run your first federated +learning system. -.. code-block:: shell +.. note:: - $ python client.py + Check the source code of the extended version of this tutorial in + |quickstart_tf_link|_ in the Flower GitHub repository. -Open another terminal and start the second client: +.. |quickstart_tf_link| replace:: -.. code-block:: shell + :code:`examples/quickstart-tensorflow` - $ python client.py +.. _quickstart_tf_link: https://github.com/adap/flower/blob/main/examples/quickstart-tensorflow -Each client will have its own dataset. +**************** + Video tutorial +**************** -You should now see how the training does in the very first terminal (the one -that started the server): +.. note:: -.. code-block:: shell + The video shown below shows how to setup a TensorFlow + Flower + project using our previously recommended APIs. A new video tutorial + will be released that shows the new APIs (as the content above does) - INFO flower 2021-02-25 14:15:46,741 | app.py:76 | Flower server running (insecure, 3 rounds) - INFO flower 2021-02-25 14:15:46,742 | server.py:72 | Getting initial parameters - INFO flower 2021-02-25 14:16:01,770 | server.py:74 | Evaluating initial parameters - INFO flower 2021-02-25 14:16:01,770 | server.py:87 | [TIME] FL starting - DEBUG flower 2021-02-25 14:16:12,341 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:21:17,235 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:21:17,512 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:21:29,628 | server.py:149 | evaluate received 2 results and 0 failures - DEBUG flower 2021-02-25 14:21:29,696 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:25:59,917 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:26:00,227 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:26:11,457 | server.py:149 | evaluate received 2 results and 0 failures - DEBUG flower 2021-02-25 14:26:11,530 | server.py:165 | fit_round: strategy sampled 2 clients (out of 2) - DEBUG flower 2021-02-25 14:30:43,389 | server.py:177 | fit_round received 2 results and 0 failures - DEBUG flower 2021-02-25 14:30:43,630 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:30:53,384 | server.py:149 | evaluate received 2 results and 0 failures - INFO flower 2021-02-25 14:30:53,384 | server.py:122 | [TIME] FL finished in 891.6143046000007 - INFO flower 2021-02-25 14:30:53,385 | app.py:109 | app_fit: losses_distributed [(1, 2.3196680545806885), (2, 2.3202896118164062), (3, 2.1818180084228516)] - INFO flower 2021-02-25 14:30:53,385 | app.py:110 | app_fit: accuracies_distributed [] - INFO flower 2021-02-25 14:30:53,385 | app.py:111 | app_fit: losses_centralized [] - INFO flower 2021-02-25 14:30:53,385 | app.py:112 | app_fit: accuracies_centralized [] - DEBUG flower 2021-02-25 14:30:53,442 | server.py:139 | evaluate: strategy sampled 2 clients - DEBUG flower 2021-02-25 14:31:02,848 | server.py:149 | evaluate received 2 results and 0 failures - INFO flower 2021-02-25 14:31:02,848 | app.py:121 | app_evaluate: federated loss: 2.1818180084228516 - INFO flower 2021-02-25 14:31:02,848 | app.py:125 | app_evaluate: results [('ipv4:127.0.0.1:57158', EvaluateRes(loss=2.1818180084228516, num_examples=10000, accuracy=0.0, metrics={'accuracy': 0.21610000729560852})), ('ipv4:127.0.0.1:57160', EvaluateRes(loss=2.1818180084228516, num_examples=10000, accuracy=0.0, metrics={'accuracy': 0.21610000729560852}))] - INFO flower 2021-02-25 14:31:02,848 | app.py:127 | app_evaluate: failures [] flower 2020-07-15 10:07:56,396 | app.py:77 | app_evaluate: failures [] +.. meta:: + :description: Check out this Federated Learning quickstart tutorial for using Flower with TensorFlow to train a CNN model on CIFAR-10. -Congratulations! You've successfully built and run your first federated -learning system. The full `source code `_ for this can be found in -:code:`examples/quickstart-tensorflow/client.py`. +.. youtube:: FGTc2TQq7VM + :width: 100% From 87712d46ad6cd4256571b83803d12721d56580fb Mon Sep 17 00:00:00 2001 From: Chong Shen Ng Date: Wed, 18 Sep 2024 18:32:42 +0100 Subject: [PATCH 07/16] docs(framework) Update quickstart tutorial documentation for Hugging Face with `flwr run` (#3341) Co-authored-by: Javier --- .../tutorial-quickstart-huggingface.rst | 634 ++++++++++++------ 1 file changed, 421 insertions(+), 213 deletions(-) diff --git a/doc/source/tutorial-quickstart-huggingface.rst b/doc/source/tutorial-quickstart-huggingface.rst index 7d8128230901..e5caa3b19dd6 100644 --- a/doc/source/tutorial-quickstart-huggingface.rst +++ b/doc/source/tutorial-quickstart-huggingface.rst @@ -1,229 +1,437 @@ .. _quickstart-huggingface: +########################### + Quickstart 🤗 Transformers +########################### + +In this federated learning tutorial we will learn how to train a large +language model (LLM) on the `IMDB +`_ dataset using +Flower and the 🤗 Hugging Face Transformers library. 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+🤗 Hugging Face +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 |flowerdatasets|_'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:: shell + + # 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 ``HuggingFace``), give a name to your +project, and type in your developer name: + +.. code:: shell + + $ 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:: 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:: shell + + # From the directory where your pyproject.toml is + $ pip install -e . + +To run the project, do: + +.. code:: shell + + # Run with default arguments + $ flwr run . + +With default arguments you will see an output like this one: + +.. code:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Starting evaluation of initial global parameters + INFO : Evaluation returned no results (`None`) + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 2 clients (out of 10) + INFO : aggregate_fit: received 2 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 249.11s + INFO : History (loss, distributed): + INFO : round 1: 0.02111011856794357 + INFO : round 2: 0.019722302150726317 + INFO : round 3: 0.018227258533239362 + INFO : + +You can also run the project with GPU as follows: + +.. code:: shell + + # Run with default arguments + $ flwr run . localhost-gpu + +This will use the default arguments where each ``ClientApp`` will use 2 +CPUs and at most 4 ``ClientApp``\s will run in a given GPU. + +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 fraction-fit=0.2" + +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 |flowerdatasets|_ to easily download and partition +the `IMDB `_ dataset. +In this example you'll make use of the |iidpartitioner|_ to generate +``num_partitions`` partitions. You can choose |otherpartitioners|_ +available in Flower Datasets. To tokenize the text, we will also load +the tokenizer from the pre-trained Transformer model that we'll use +during training - more on that in the next section. 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="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) + + def tokenize_function(examples): + return tokenizer( + examples["text"], truncation=True, add_special_tokens=True, max_length=512 + ) + + 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 + ) + +*********** + The Model +*********** + +We will leverage 🤗 Hugging Face to federate the training of language +models over multiple clients using Flower. More specifically, we will +fine-tune a pre-trained Transformer model (|berttiny|_) for sequence +classification over the dataset of IMDB ratings. The end goal is to +detect if a movie rating is positive or negative. If you have access to +larger GPUs, feel free to use larger models! + +.. code:: python + + net = AutoModelForSequenceClassification.from_pretrained( + model_name, num_labels=num_labels + ) + +Note that here, ``model_name`` is a string that will be loaded from the +``Context`` in the ClientApp and ServerApp. + +In addition to loading the pretrained model weights and 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): + 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): + 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 + +*************** + The ClientApp +*************** + +The main changes we have to make to use 🤗 Hugging Face with Flower will +be found in the ``get_weights()`` and ``set_weights()`` functions. Under +the hood, the ``transformers`` library uses PyTorch, which means we can +reuse the ``get_weights()`` and ``set_weights()`` code that we defined +in the :doc:`Quickstart PyTorch ` tutorial. +As a reminder, in ``get_weights()``, PyTorch model parameters are +extracted and represented as a list of NumPy arrays. The +``set_weights()`` function that's the opposite: 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 ``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, testloader, local_epochs): + self.net = net + self.trainloader = trainloader + self.testloader = testloader + 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) + train(self.net, self.trainloader, epochs=self.local_epochs, device=self.device) + return get_weights(self.net), len(self.trainloader), {} + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.testloader, self.device) + return float(loss), len(self.testloader), {"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 +additional hyperparameters in ``pyproject.toml`` and access them here. + +.. code:: python -Quickstart 🤗 Transformers -========================== + def client_fn(context: Context): -.. meta:: - :description: Check out this Federating Learning quickstart tutorial for using Flower with HuggingFace Transformers in order to fine-tune an LLM. - -Let's build a federated learning system using Hugging Face Transformers and Flower! - -We will leverage Hugging Face to federate the training of language models over multiple clients using Flower. -More specifically, we will fine-tune a pre-trained Transformer model (distilBERT) -for sequence classification over a dataset of IMDB ratings. -The end goal is to detect if a movie rating is positive or negative. - -Dependencies ------------- - -To follow along this tutorial you will need to install the following packages: -:code:`datasets`, :code:`evaluate`, :code:`flwr`, :code:`torch`, and :code:`transformers`. -This can be done using :code:`pip`: - -.. code-block:: shell - - $ pip install datasets evaluate flwr torch transformers - - -Standard Hugging Face workflow ------------------------------- - -Handling the data -^^^^^^^^^^^^^^^^^ - -To fetch the IMDB dataset, we will use Hugging Face's :code:`datasets` library. -We then need to tokenize the data and create :code:`PyTorch` dataloaders, -this is all done in the :code:`load_data` function: - -.. code-block:: python - - import random - import torch - from datasets import load_dataset - from torch.utils.data import DataLoader - from transformers import AutoTokenizer, DataCollatorWithPadding - - DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - CHECKPOINT = "distilbert-base-uncased" - - def load_data(): - """Load IMDB data (training and eval)""" - raw_datasets = load_dataset("imdb") - raw_datasets = raw_datasets.shuffle(seed=42) - # remove unnecessary data split - del raw_datasets["unsupervised"] - tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT) - def tokenize_function(examples): - return tokenizer(examples["text"], truncation=True) - # We will take a small sample in order to reduce the compute time, this is optional - train_population = random.sample(range(len(raw_datasets["train"])), 100) - test_population = random.sample(range(len(raw_datasets["test"])), 100) - tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) - tokenized_datasets["train"] = tokenized_datasets["train"].select(train_population) - tokenized_datasets["test"] = tokenized_datasets["test"].select(test_population) - tokenized_datasets = tokenized_datasets.remove_columns("text") - tokenized_datasets = tokenized_datasets.rename_column("label", "labels") - data_collator = DataCollatorWithPadding(tokenizer=tokenizer) - trainloader = DataLoader( - tokenized_datasets["train"], - shuffle=True, - batch_size=32, - collate_fn=data_collator, - ) - testloader = DataLoader( - tokenized_datasets["test"], batch_size=32, collate_fn=data_collator - ) - return trainloader, testloader - - -Training and testing the model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Once we have a way of creating our trainloader and testloader, -we can take care of the training and testing. -This is very similar to any :code:`PyTorch` training or testing loop: - -.. code-block:: python - - from evaluate import load as load_metric - from transformers import AdamW - - 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 - - -Creating the model itself -^^^^^^^^^^^^^^^^^^^^^^^^^ - -To create the model itself, -we will just load the pre-trained distillBERT model using Hugging Face’s :code:`AutoModelForSequenceClassification` : - -.. code-block:: python - - from transformers import AutoModelForSequenceClassification - - net = AutoModelForSequenceClassification.from_pretrained( - CHECKPOINT, num_labels=2 - ).to(DEVICE) - - -Federating the example ----------------------- - -Creating the IMDBClient -^^^^^^^^^^^^^^^^^^^^^^^ - -To federate our example to multiple clients, -we first need to write our Flower client class (inheriting from :code:`flwr.client.NumPyClient`). -This is very easy, as our model is a standard :code:`PyTorch` model: - -.. code-block:: python - - from collections import OrderedDict - import flwr as fl - - 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)} - - -The :code:`get_parameters` function lets the server get the client's parameters. -Inversely, the :code:`set_parameters` function allows the server to send its parameters to the client. -Finally, the :code:`fit` function trains the model locally for the client, -and the :code:`evaluate` function tests the model locally and returns the relevant metrics. - -Starting the server -^^^^^^^^^^^^^^^^^^^ + # Get this client's dataset partition + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] + model_name = context.run_config["model-name"] + trainloader, valloader = load_data(partition_id, num_partitions, model_name) + + # Load model + num_labels = context.run_config["num-labels"] + net = AutoModelForSequenceClassification.from_pretrained( + model_name, num_labels=num_labels + ) + + 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` strategy. 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 global model + model_name = context.run_config["model-name"] + num_labels = context.run_config["num-labels"] + net = AutoModelForSequenceClassification.from_pretrained( + model_name, num_labels=num_labels + ) + + weights = get_weights(net) + initial_parameters = ndarrays_to_parameters(weights) + + # Define strategy + strategy = FedAvg( + fraction_fit=fraction_fit, + fraction_evaluate=1.0, + initial_parameters=initial_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 for an LLM. + +.. note:: + + Check the source code of the extended version of this tutorial in + |quickstart_hf_link|_ in the Flower GitHub repository. For a + comprehensive example of a federated fine-tuning of an LLM with + Flower, refer to the |flowertune|_ example in the Flower GitHub + repository. -Now that we have a way to instantiate clients, we need to create our server in order to aggregate the results. -Using Flower, this can be done very easily by first choosing a strategy (here, we are using :code:`FedAvg`, -which will define the global weights as the average of all the clients' weights at each round) -and then using the :code:`flwr.server.start_server` function: - -.. code-block:: python - - def weighted_average(metrics): - accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] - losses = [num_examples * m["loss"] for num_examples, m in metrics] - examples = [num_examples for num_examples, _ in metrics] - return {"accuracy": sum(accuracies) / sum(examples), "loss": sum(losses) / sum(examples)} - - # Define strategy - strategy = fl.server.strategy.FedAvg( - fraction_fit=1.0, - fraction_evaluate=1.0, - evaluate_metrics_aggregation_fn=weighted_average, - ) - - # Start server - fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=3), - strategy=strategy, - ) +.. |quickstart_hf_link| replace:: + ``examples/quickstart-huggingface`` -The :code:`weighted_average` function is there to provide a way to aggregate the metrics distributed amongst -the clients (basically this allows us to display a nice average accuracy and loss for every round). +.. |fedavg| replace:: -Putting everything together ---------------------------- + ``FedAvg`` -We can now start client instances using: +.. |iidpartitioner| replace:: -.. code-block:: python + ``IidPartitioner`` - fl.client.start_client( - server_address="127.0.0.1:8080", - client=IMDBClient().to_client() - ) +.. |otherpartitioners| replace:: + other partitioners -And they will be able to connect to the server and start the federated training. +.. |berttiny| replace:: -If you want to check out everything put together, -you should check out the `full code example `_ . + ``bert-tiny`` -Of course, this is a very basic example, and a lot can be added or modified, -it was just to showcase how simply we could federate a Hugging Face workflow using Flower. +.. |serverappcomponents| replace:: -Note that in this example we used :code:`PyTorch`, but we could have very well used :code:`TensorFlow`. + ``ServerAppComponents`` + +.. |client| replace:: + + ``Client`` + +.. |flowerdatasets| replace:: + + Flower Datasets + +.. |flowertune| replace:: + + FlowerTune LLM + +.. _berttiny: https://huggingface.co/prajjwal1/bert-tiny + +.. _client: ref-api/flwr.client.Client.html#client + +.. _fedavg: ref-api/flwr.server.strategy.FedAvg.html#flwr.server.strategy.FedAvg + +.. _flowerdatasets: https://flower.ai/docs/datasets/ + +.. _flowertune: https://github.com/adap/flower/tree/main/examples/flowertune-llm + +.. _iidpartitioner: https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.IidPartitioner.html#flwr_datasets.partitioner.IidPartitioner + +.. _otherpartitioners: https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.html + +.. _quickstart_hf_link: https://github.com/adap/flower/tree/main/examples/quickstart-huggingface + +.. _serverappcomponents: ref-api/flwr.server.ServerAppComponents.html#serverappcomponents + +.. meta:: + :description: Check out this Federating Learning quickstart tutorial for using Flower with 🤗 HuggingFace Transformers in order to fine-tune an LLM. From 13d5f4068d9ac2832bcbec875122ba2daceb1d48 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 18 Sep 2024 20:12:05 +0200 Subject: [PATCH 08/16] docs(framework) Update fastai quickstart tutorial (#4151) --- doc/source/tutorial-quickstart-fastai.rst | 113 ++++++++++++++++++++-- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/doc/source/tutorial-quickstart-fastai.rst b/doc/source/tutorial-quickstart-fastai.rst index 63f5ac176082..e42328e6f712 100644 --- a/doc/source/tutorial-quickstart-fastai.rst +++ b/doc/source/tutorial-quickstart-fastai.rst @@ -1,12 +1,113 @@ .. _quickstart-fastai: +################### + Quickstart fastai +################### -Quickstart fastai -================= +In this federated learning tutorial we will learn how to train a +SqueezeNet model on MNIST using Flower and fastai. It is recommended to +create a virtual environment and run everything within a +:doc:`virtualenv `. -.. meta:: - :description: Check out this Federated Learning quickstart tutorial for using Flower with FastAI to train a vision model on CIFAR-10. +Then, clone the code example directly from GitHub: -Let's build a federated learning system using fastai and Flower! +.. code:: shell -Please refer to the `full code example `_ to learn more. + 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: + +.. code:: shell + + 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 + +Next, activate your environment, then run: + +.. code:: shell + + # Navigate to the example directory + $ cd path/to/quickstart-fastai + + # Install project and dependencies + $ pip install -e . + +This example by default runs the Flower Simulation Engine, creating a +federation of 10 nodes using `FedAvg +`_ +as the aggregation strategy. The dataset will be partitioned using +Flower Dataset's `IidPartitioner +`_. +Let's run the project: + +.. code:: shell + + # Run with default arguments + $ flwr run . + +With default arguments you will see an output like this one: + +.. code:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Starting evaluation of initial global parameters + INFO : Evaluation returned no results (`None`) + 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 5 clients (out of 10) + INFO : aggregate_evaluate: received 5 results and 0 failures + 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 5 clients (out of 10) + INFO : aggregate_evaluate: received 5 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 5 clients (out of 10) + INFO : aggregate_evaluate: received 5 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 143.02s + INFO : History (loss, distributed): + INFO : round 1: 2.699497365951538 + INFO : round 2: 0.9549586296081543 + INFO : round 3: 0.6627192616462707 + INFO : History (metrics, distributed, evaluate): + INFO : {'accuracy': [(1, 0.09766666889190674), + INFO : (2, 0.6948333323001862), + INFO : (3, 0.7721666693687439)]} + 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 + +.. note:: + + Check the `source code + `_ + of this tutorial in ``examples/quickstart-fasai`` in the Flower + GitHub repository. From c9d2ff683415d059d2dafd8020c024d3c195e82c Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 18 Sep 2024 20:24:45 +0200 Subject: [PATCH 09/16] docs(framework) Update pytorch-lightning quickstart tutorial (#4152) --- .../tutorial-quickstart-pytorch-lightning.rst | 119 +++++++++++++++++- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/doc/source/tutorial-quickstart-pytorch-lightning.rst b/doc/source/tutorial-quickstart-pytorch-lightning.rst index acfbecf41260..7c74c9a1682f 100644 --- a/doc/source/tutorial-quickstart-pytorch-lightning.rst +++ b/doc/source/tutorial-quickstart-pytorch-lightning.rst @@ -1,12 +1,119 @@ .. _quickstart-pytorch-lightning: +############################## + Quickstart PyTorch Lightning +############################## -Quickstart PyTorch Lightning -============================ +In this federated learning tutorial we will learn how to train an +AutoEncoder model on MNIST using Flower and PyTorch Lightning. It is +recommended to create a virtual environment and run everything within a +:doc:`virtualenv `. -.. meta:: - :description: Check out this Federated Learning quickstart tutorial for using Flower with PyTorch Lightning to train an Auto Encoder model on MNIST. +Then, clone the code example directly from GitHub: -Let's build a horizontal federated learning system using PyTorch Lightning and Flower! +.. code:: shell -Please refer to the `full code example `_ to learn more. + 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: + +.. code:: shell + + quickstart-pytorch-lightning + ├── pytorchlightning_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 + +Next, activate your environment, then run: + +.. code:: shell + + # Navigate to the example directory + $ cd path/to/quickstart-pytorch-lightning + + # Install project and dependencies + $ pip install -e . + +By default, Flower Simulation Engine will be started and it will create +a federation of 4 nodes using `FedAvg +`_ +as the aggregation strategy. The dataset will be partitioned using +Flower Dataset's `IidPartitioner +`_. +To run the project, do: + +.. code:: shell + + # Run with default arguments + $ flwr run . + +With default arguments you will see an output like this one: + +.. code:: shell + + Loading project configuration... + Success + INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout + INFO : + INFO : [INIT] + INFO : Using initial global parameters provided by strategy + INFO : Starting evaluation of initial global parameters + INFO : Evaluation returned no results (`None`) + INFO : + INFO : [ROUND 1] + INFO : configure_fit: strategy sampled 2 clients (out of 4) + INFO : aggregate_evaluate: received 2 results and 0 failures + WARNING : No evaluate_metrics_aggregation_fn provided + INFO : + INFO : [ROUND 2] + INFO : configure_fit: strategy sampled 2 clients (out of 4) + INFO : aggregate_fit: received 2 results and 0 failures + INFO : configure_evaluate: strategy sampled 2 clients (out of 4) + INFO : aggregate_evaluate: received 2 results and 0 failures + INFO : + INFO : [ROUND 3] + INFO : configure_fit: strategy sampled 2 clients (out of 4) + INFO : aggregate_fit: received 2 results and 0 failures + INFO : configure_evaluate: strategy sampled 2 clients (out of 4) + INFO : aggregate_evaluate: received 2 results and 0 failures + INFO : + INFO : [SUMMARY] + INFO : Run finished 3 round(s) in 136.92s + INFO : History (loss, distributed): + INFO : round 1: 0.04982871934771538 + INFO : round 2: 0.046457378193736076 + INFO : round 3: 0.04506748169660568 + INFO : + +Each simulated `ClientApp` (two per round) will also log a summary of +their local training process. Expect this output to be similar to: + +.. code:: shell + + # The left part indicates the process ID running the `ClientApp` + (ClientAppActor pid=38155) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + (ClientAppActor pid=38155) ┃ Test metric ┃ DataLoader 0 ┃ + (ClientAppActor pid=38155) ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + (ClientAppActor pid=38155) │ test_loss │ 0.045175597071647644 │ + (ClientAppActor pid=38155) └───────────────────────────┴───────────────────────────┘ + +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 + +.. note:: + + Check the `source code + `_ + of this tutorial in ``examples/quickstart-pytorch-lightning`` in the + Flower GitHub repository. From f60658a6b3157926965f1e8d65bad096e20ea8ec Mon Sep 17 00:00:00 2001 From: Adam Narozniak <51029327+adam-narozniak@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:54:24 +0200 Subject: [PATCH 10/16] feat(datasets) Add SizePartitioner (#4111) Co-authored-by: jafermarq --- .../flwr_datasets/partitioner/__init__.py | 2 + .../partitioner/size_partitioner.py | 128 ++++++ .../partitioner/size_partitioner_test.py | 392 ++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 datasets/flwr_datasets/partitioner/size_partitioner.py create mode 100644 datasets/flwr_datasets/partitioner/size_partitioner_test.py diff --git a/datasets/flwr_datasets/partitioner/__init__.py b/datasets/flwr_datasets/partitioner/__init__.py index 3fed4446db42..a14efa1cc905 100644 --- a/datasets/flwr_datasets/partitioner/__init__.py +++ b/datasets/flwr_datasets/partitioner/__init__.py @@ -27,6 +27,7 @@ from .partitioner import Partitioner from .pathological_partitioner import PathologicalPartitioner from .shard_partitioner import ShardPartitioner +from .size_partitioner import SizePartitioner from .square_partitioner import SquarePartitioner __all__ = [ @@ -42,5 +43,6 @@ "Partitioner", "PathologicalPartitioner", "ShardPartitioner", + "SizePartitioner", "SquarePartitioner", ] diff --git a/datasets/flwr_datasets/partitioner/size_partitioner.py b/datasets/flwr_datasets/partitioner/size_partitioner.py new file mode 100644 index 000000000000..a79b6b7249f2 --- /dev/null +++ b/datasets/flwr_datasets/partitioner/size_partitioner.py @@ -0,0 +1,128 @@ +# 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. +# ============================================================================== +"""SizePartitioner class.""" + + +import warnings +from collections.abc import Sequence + +import datasets +from flwr_datasets.partitioner.partitioner import Partitioner + + +class SizePartitioner(Partitioner): + """Partitioner that creates each partition with the size specified by a user. + + Parameters + ---------- + partition_sizes : Sequence[int] + The size of each partition. partition_id 0 will have partition_sizes[0] + samples, partition_id 1 will have partition_sizes[1] samples, etc. + + Examples + -------- + >>> from flwr_datasets import FederatedDataset + >>> from flwr_datasets.partitioner import SizePartitioner + >>> + >>> partition_sizes = [15_000, 5_000, 30_000] + >>> partitioner = SizePartitioner(partition_sizes) + >>> fds = FederatedDataset(dataset="cifar10", partitioners={"train": partitioner}) + """ + + def __init__(self, partition_sizes: Sequence[int]) -> None: + super().__init__() + self._pre_ds_validate_partition_sizes(partition_sizes) + self._partition_sizes = partition_sizes + self._partition_id_to_indices: dict[int, list[int]] = {} + self._partition_id_to_indices_determined = False + + def load_partition(self, partition_id: int) -> datasets.Dataset: + """Load a single partition of the size of partition_sizes[partition_id]. + + For example if given partition_sizes=[20_000, 10_000, 30_000], + then partition_id=0 will return a partition of size 20_000, + partition_id=1 will return a partition of size 10_000, etc. + + Parameters + ---------- + partition_id : int + The index that corresponds to the requested partition. + + Returns + ------- + dataset_partition : Dataset + Single dataset partition. + """ + self._determine_partition_id_to_indices_if_needed() + return self.dataset.select(self._partition_id_to_indices[partition_id]) + + @property + def num_partitions(self) -> int: + """Total number of partitions.""" + self._determine_partition_id_to_indices_if_needed() + return len(self._partition_sizes) + + @property + def partition_id_to_indices(self) -> dict[int, list[int]]: + """Partition id to indices (the result of partitioning).""" + self._determine_partition_id_to_indices_if_needed() + return self._partition_id_to_indices + + def _determine_partition_id_to_indices_if_needed( + self, + ) -> None: + """Create an assignment of indices to the partition indices.""" + if self._partition_id_to_indices_determined: + return + self._post_ds_validate_partition_sizes() + start = 0 + end = 0 + for partition_id, partition_size in enumerate(self._partition_sizes): + end += partition_size + indices = list(range(start, end)) + self._partition_id_to_indices[partition_id] = indices + start = end + self._partition_id_to_indices_determined = True + + def _pre_ds_validate_partition_sizes(self, partition_sizes: Sequence[int]) -> None: + """Check if the partition sizes are valid (no information about the dataset).""" + if not isinstance(partition_sizes, Sequence): + raise ValueError("Partition sizes must be a sequence.") + if len(partition_sizes) == 0: + raise ValueError("Partition sizes must not be empty.") + if not all( + isinstance(partition_size, int) for partition_size in partition_sizes + ): + raise ValueError("All partition sizes must be integers.") + if not all(partition_size > 0 for partition_size in partition_sizes): + raise ValueError("All partition sizes must be greater than zero.") + + def _post_ds_validate_partition_sizes(self) -> None: + """Validate the partition sizes against the dataset size.""" + desired_partition_sizes = sum(self._partition_sizes) + dataset_size = len(self.dataset) + if desired_partition_sizes > dataset_size: + raise ValueError( + f"The sum of partition sizes sum({self._partition_sizes})" + f"= {desired_partition_sizes} is greater than the size of" + f" the dataset {dataset_size}." + ) + if desired_partition_sizes < dataset_size: + warnings.warn( + f"The sum of partition sizes is {desired_partition_sizes}, which is" + f"smaller than the size of the dataset: {dataset_size}. " + f"Ignore this warning if it is the desired behavior.", + stacklevel=1, + ) diff --git a/datasets/flwr_datasets/partitioner/size_partitioner_test.py b/datasets/flwr_datasets/partitioner/size_partitioner_test.py new file mode 100644 index 000000000000..be8edf9d2764 --- /dev/null +++ b/datasets/flwr_datasets/partitioner/size_partitioner_test.py @@ -0,0 +1,392 @@ +# Copyright 2023 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 ShardPartitioner.""" + + +# pylint: disable=W0212, R0913 +import unittest +from typing import Optional + +from datasets import Dataset +from flwr_datasets.partitioner.shard_partitioner import ShardPartitioner + + +def _dummy_setup( + num_rows: int, + partition_by: str, + num_partitions: int, + num_shards_per_partition: Optional[int], + shard_size: Optional[int], + keep_incomplete_shard: bool = False, +) -> tuple[Dataset, ShardPartitioner]: + """Create a dummy dataset for testing.""" + data = { + partition_by: [i % 3 for i in range(num_rows)], + "features": list(range(num_rows)), + } + dataset = Dataset.from_dict(data) + partitioner = ShardPartitioner( + num_partitions=num_partitions, + num_shards_per_partition=num_shards_per_partition, + partition_by=partition_by, + shard_size=shard_size, + keep_incomplete_shard=keep_incomplete_shard, + ) + partitioner.dataset = dataset + return dataset, partitioner + + +class TestShardPartitionerSpec1(unittest.TestCase): + """Test first possible initialization of ShardPartitioner. + + Specify num_shards_per_partition and shard_size arguments. + """ + + def test_correct_num_partitions(self) -> None: + """Test the correct number of partitions is created.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + _ = partitioner.load_partition(0) + num_partitions_created = len(partitioner._partition_id_to_indices.keys()) + self.assertEqual(num_partitions_created, num_partitions) + + def test_correct_partition_sizes(self) -> None: + """Test if the partitions sizes are as theoretically calculated.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + sizes = [len(partitioner.load_partition(i)) for i in range(num_partitions)] + sizes = sorted(sizes) + self.assertEqual(sizes, [30, 30, 30]) + + def test_unique_samples(self) -> None: + """Test if each partition has unique samples. + + (No duplicates along partitions). + """ + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + partitions = [ + partitioner.load_partition(i)["features"] for i in range(num_partitions) + ] + combined_list = [item for sublist in partitions for item in sublist] + combined_set = set(combined_list) + self.assertEqual(len(combined_list), len(combined_set)) + + +class TestShardPartitionerSpec2(unittest.TestCase): + """Test second possible initialization of ShardPartitioner. + + Specify shard_size and keep_incomplete_shard=False. This setting creates partitions + that might have various sizes (each shard is same size). + """ + + def test_correct_num_partitions(self) -> None: + """Test the correct number of partitions is created.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + _ = partitioner.load_partition(0) + num_partitions_created = len(partitioner._partition_id_to_indices.keys()) + self.assertEqual(num_partitions_created, num_partitions) + + def test_correct_partition_sizes(self) -> None: + """Test if the partitions sizes are as theoretically calculated.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + sizes = [len(partitioner.load_partition(i)) for i in range(num_partitions)] + sizes = sorted(sizes) + self.assertEqual(sizes, [30, 40, 40]) + + def test_unique_samples(self) -> None: + """Test if each partition has unique samples. + + (No duplicates along partitions). + """ + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + partitions = [ + partitioner.load_partition(i)["features"] for i in range(num_partitions) + ] + combined_list = [item for sublist in partitions for item in sublist] + combined_set = set(combined_list) + self.assertEqual(len(combined_list), len(combined_set)) + + +class TestShardPartitionerSpec3(unittest.TestCase): + """Test third possible initialization of ShardPartitioner. + + Specify shard_size and keep_incomplete_shard=True. This setting creates partitions + that might have various sizes (each shard is same size). + """ + + def test_correct_num_partitions(self) -> None: + """Test the correct number of partitions is created.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = True + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + _ = partitioner.load_partition(0) + num_partitions_created = len(partitioner._partition_id_to_indices.keys()) + self.assertEqual(num_partitions_created, num_partitions) + + def test_correct_partition_sizes(self) -> None: + """Test if the partitions sizes are as theoretically calculated.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = True + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + sizes = [len(partitioner.load_partition(i)) for i in range(num_partitions)] + sizes = sorted(sizes) + self.assertEqual(sizes, [33, 40, 40]) + + def test_unique_samples(self) -> None: + """Test if each partition has unique samples. + + (No duplicates along partitions). + """ + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = True + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + partitions = [ + partitioner.load_partition(i)["features"] for i in range(num_partitions) + ] + combined_list = [item for sublist in partitions for item in sublist] + combined_set = set(combined_list) + self.assertEqual(len(combined_list), len(combined_set)) + + +class TestShardPartitionerSpec4(unittest.TestCase): + """Test fourth possible initialization of ShardPartitioner. + + Specify num_shards_per_partition but not shard_size arguments. + """ + + def test_correct_num_partitions(self) -> None: + """Test the correct number of partitions is created.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = None + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + _ = partitioner.load_partition(0) + num_partitions_created = len(partitioner._partition_id_to_indices.keys()) + self.assertEqual(num_partitions_created, num_partitions) + + def test_correct_partition_sizes(self) -> None: + """Test if the partitions sizes are as theoretically calculated.""" + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = None + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + sizes = [len(partitioner.load_partition(i)) for i in range(num_partitions)] + sizes = sorted(sizes) + self.assertEqual(sizes, [36, 36, 36]) + + def test_unique_samples(self) -> None: + """Test if each partition has unique samples. + + (No duplicates along partitions). + """ + partition_by = "label" + num_rows = 113 + num_partitions = 3 + num_shards_per_partition = 3 + shard_size = None + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + partitions = [ + partitioner.load_partition(i)["features"] for i in range(num_partitions) + ] + combined_list = [item for sublist in partitions for item in sublist] + combined_set = set(combined_list) + self.assertEqual(len(combined_list), len(combined_set)) + + +class TestShardPartitionerIncorrectSpec(unittest.TestCase): + """Test the incorrect specification cases. + + The lack of correctness can be caused by the num_partitions, shard_size and + num_shards_per_partition can create. + """ + + def test_incorrect_specification(self) -> None: + """Test if the given specification makes the partitioning possible.""" + partition_by = "label" + num_rows = 10 + num_partitions = 3 + num_shards_per_partition = 2 + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + with self.assertRaises(ValueError): + _ = partitioner.load_partition(0) + + def test_too_big_shard_size(self) -> None: + """Test if it is impossible to create an empty partition.""" + partition_by = "label" + num_rows = 20 + num_partitions = 3 + num_shards_per_partition = None + shard_size = 10 + keep_incomplete_shard = False + _, partitioner = _dummy_setup( + num_rows, + partition_by, + num_partitions, + num_shards_per_partition, + shard_size, + keep_incomplete_shard, + ) + with self.assertRaises(ValueError): + _ = partitioner.load_partition(2).num_rows + + +if __name__ == "__main__": + unittest.main() From 22875a74ac0747333f91d97ba16ba0d35d1b0397 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 19 Sep 2024 10:25:58 +0200 Subject: [PATCH 11/16] refactor(examples) Update `advanced-pytorch` example (#4130) Co-authored-by: Chong Shen Ng --- examples/advanced-pytorch/.gitignore | 3 + examples/advanced-pytorch/README.md | 105 +++++++----- .../_static/fmnist_50_lda.png | Bin 0 -> 33801 bytes .../advanced-pytorch/_static/wandb_plots.png | Bin 0 -> 251711 bytes examples/advanced-pytorch/client.py | 160 ------------------ examples/advanced-pytorch/pyproject.toml | 58 +++++-- .../pytorch_example/__init__.py | 1 + .../pytorch_example/client_app.py | 122 +++++++++++++ .../pytorch_example/server_app.py | 96 +++++++++++ .../pytorch_example/strategy.py | 116 +++++++++++++ .../advanced-pytorch/pytorch_example/task.py | 159 +++++++++++++++++ examples/advanced-pytorch/requirements.txt | 5 - examples/advanced-pytorch/run.sh | 16 -- examples/advanced-pytorch/server.py | 121 ------------- examples/advanced-pytorch/utils.py | 117 ------------- 15 files changed, 598 insertions(+), 481 deletions(-) create mode 100644 examples/advanced-pytorch/.gitignore create mode 100644 examples/advanced-pytorch/_static/fmnist_50_lda.png create mode 100644 examples/advanced-pytorch/_static/wandb_plots.png delete mode 100644 examples/advanced-pytorch/client.py create mode 100644 examples/advanced-pytorch/pytorch_example/__init__.py create mode 100644 examples/advanced-pytorch/pytorch_example/client_app.py create mode 100644 examples/advanced-pytorch/pytorch_example/server_app.py create mode 100644 examples/advanced-pytorch/pytorch_example/strategy.py create mode 100644 examples/advanced-pytorch/pytorch_example/task.py delete mode 100644 examples/advanced-pytorch/requirements.txt delete mode 100755 examples/advanced-pytorch/run.sh delete mode 100644 examples/advanced-pytorch/server.py delete mode 100644 examples/advanced-pytorch/utils.py diff --git a/examples/advanced-pytorch/.gitignore b/examples/advanced-pytorch/.gitignore new file mode 100644 index 000000000000..014ee796bf45 --- /dev/null +++ b/examples/advanced-pytorch/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +outputs/ +wandb/ diff --git a/examples/advanced-pytorch/README.md b/examples/advanced-pytorch/README.md index ac0737673407..1771173c3925 100644 --- a/examples/advanced-pytorch/README.md +++ b/examples/advanced-pytorch/README.md @@ -1,77 +1,90 @@ --- -tags: [advanced, vision, fds] -dataset: [CIFAR-10] +tags: [advanced, vision, fds, wandb] +dataset: [Fashion-MNIST] framework: [torch, torchvision] --- -# Advanced Flower Example (PyTorch) +# Federated Learning with PyTorch and Flower (Advanced Example) -This example demonstrates an advanced federated learning setup using Flower with PyTorch. This example uses [Flower Datasets](https://flower.ai/docs/datasets/) and it differs from the quickstart example in the following ways: +> \[!TIP\] +> This example shows intermediate and advanced functionality of Flower. It you are new to Flower, it is recommended to start from the [quickstart-pytorch](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example or the [quickstart PyTorch tutorial](https://flower.ai/docs/framework/tutorial-quickstart-pytorch.html). -- 10 clients (instead of just 2) -- Each client holds a local dataset of 5000 training examples and 1000 test examples (note that using the `run.sh` script will only select 10 data samples by default, as the `--toy` argument is set). -- Server-side model evaluation after parameter aggregation -- Hyperparameter schedule using config functions -- Custom return values -- Server-side parameter initialization +This example shows how to extend your `ClientApp` and `ServerApp` capabilities compared to what's shown in the [`quickstart-pytorch`](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example. In particular, it will show how the `ClientApp`'s state (and object of type [RecordSet](https://flower.ai/docs/framework/ref-api/flwr.common.RecordSet.html)) can be used to enable stateful clients, facilitating the design of personalized federated learning strategies, among others. The `ServerApp` in this example makes use of a custom strategy derived from the built-in [FedAvg](https://flower.ai/docs/framework/ref-api/flwr.server.strategy.FedAvg.html). In addition, it will also showcase how to: -## Project Setup +1. Save model checkpoints +2. Save the metrics available at the strategy (e.g. accuracies, losses) +3. Log training artefacts to [Weights & Biases](https://wandb.ai/site) +4. Implement a simple decaying learning rate schedule across rounds -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: +The structure of this directory is as follows: ```shell -git clone --depth=1 https://github.com/adap/flower.git && mv flower/examples/advanced-pytorch . && rm -rf flower && cd advanced-pytorch +advanced-pytorch +├── pytorch_example +│ ├── __init__.py +│ ├── client_app.py # Defines your ClientApp +│ ├── server_app.py # Defines your ServerApp +│ ├── strategy.py # Defines a custom strategy +│ └── task.py # Defines your model, training and data loading +├── pyproject.toml # Project metadata like dependencies and configs +└── README.md ``` -This will create a new directory called `advanced-pytorch` containing the following files: +> \[!NOTE\] +> By default this example will log metrics to Weights & Biases. For this, you need to ensure that your system has logged in. Often it's as simple as executing `wandb login` on the terminal after installing `wandb`. Please, refer to this [quickstart guide](https://docs.wandb.ai/quickstart#2-log-in-to-wb) for more information. -```shell --- pyproject.toml --- requirements.txt --- client.py --- server.py --- README.md --- run.sh -``` +This examples uses [Flower Datasets](https://flower.ai/docs/datasets/) with the [Dirichlet Partitioner](https://flower.ai/docs/datasets/ref-api/flwr_datasets.partitioner.DirichletPartitioner.html#flwr_datasets.partitioner.DirichletPartitioner) to partition the [Fashion-MNIST](https://huggingface.co/datasets/zalando-datasets/fashion_mnist) dataset in a non-IID fashion into 50 partitions. -### Installing Dependencies +![](_static/fmnist_50_lda.png) -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. +> \[!TIP\] +> You can use Flower Datasets [built-in visualization tools](https://flower.ai/docs/datasets/tutorial-visualize-label-distribution.html) to easily generate plots like the one above. -#### Poetry +### Install dependencies and project -```shell -poetry install -poetry shell +Install the dependencies defined in `pyproject.toml` as well as the `pytorch_example` package. + +```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! +When you run the project, the strategy will create a directory structure in the form of `outputs/date/time` and store two `JSON` files: `config.json` containing the `run-config` that the `ServerApp` receives; and `results.json` containing the results (accuracies, losses) that are generated at the strategy. -#### pip +By default, the metrics: {`centralized_accuracy`, `centralized_loss`, `federated_evaluate_accuracy`, `federated_evaluate_loss`} will be logged to Weights & Biases (they are also stored to the `results.json` previously mentioned). Upon executing `flwr run` you'll see a URL linking to your Weight&Biases dashboard wher you can see the metrics. -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. +![](_static/wandb_plots.png) -```shell -pip install -r requirements.txt +### Run with the Simulation Engine + +With default parameters, 25% of the total 50 nodes (see `num-supernodes` in `pyproject.toml`) will be sampled for `fit` and 50% for an `evaluate` round. By default `ClientApp` objects will run on CPU. + +> \[!TIP\] +> To run your `ClientApps` on GPU or to adjust the degree or parallelism of your simulation, edit the `[tool.flwr.federations.local-simulation]` section in the `pyproject.tom`. + +```bash +flwr run . + +# To disable W&B +flwr run . --run-config use-wandb=false ``` -## Run Federated Learning with PyTorch and Flower +You can run the app using another federation (see `pyproject.toml`). For example, if you have a GPU available, select the `local-sim-gpu` federation: -The included `run.sh` will start the Flower server (using `server.py`), -sleep for 2 seconds to ensure that the server is up, and then start 10 Flower clients (using `client.py`) with only a small subset of the data (in order to run on any machine), -but this can be changed by removing the `--toy` argument in the script. You can simply start everything in a terminal as follows: +```bash +flwr run . local-sim-gpu +``` -```shell -# After activating your environment -./run.sh +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.5" ``` -The `run.sh` script starts processes in the background so that you don't have to open eleven terminal windows. If you experiment with the code example and something goes wrong, simply using `CTRL + C` on Linux (or `CMD + C` on macOS) wouldn't normally kill all these processes, which is why the script ends with `trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT` and `wait`. This simply allows you to stop the experiment using `CTRL + C` (or `CMD + C`). If you change the script and anything goes wrong you can still use `killall python` (or `killall python3`) to kill all background processes (or a more specific command if you have other Python processes running that you don't want to kill). +### Run with the Deployment Engine -You can also manually run `python3 server.py` and `python3 client.py --client-id ` for as many clients as you want but you have to make sure that each command is run in a different terminal window (or a different computer on the network). In addition, you can make your clients use either `EfficienNet` (default) or `AlexNet` (but all clients in the experiment should use the same). Switch between models using the `--model` flag when launching `client.py` and `server.py`. +> \[!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/advanced-pytorch/_static/fmnist_50_lda.png b/examples/advanced-pytorch/_static/fmnist_50_lda.png new file mode 100644 index 0000000000000000000000000000000000000000..9dfedc59a3de86c3670f0a6ab1175dcef15fd04c GIT binary patch literal 33801 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

    Hvrfe&ZRNcM-`kdDgQHy=`}kuB7&qqr z#GqHVy5FuKt#D*-aN}z}Lso5gekS1(Tw)K;hXWKnpg2^9bp@wG_0NN6?|!+O6vV(M*4w zRoX6dlPWC1Fvz|Sx#F(PAPc)QKOkIcSX?pRTc^k8+nEFZy3t|0YbIP-n|&O!vSM^a z6jC2{;}-CN`{1;HspT~WN?`{1h;Um#?aa-YUyNB4E{;^rbx&+7OFs@d8y9_Vu4r}$ z6L;?}4RN&%(umhs>F`W}^uMA`ljtsK(77bZncwsBJc_p7vutIQsa+xN*!5a7?nH&g z86O(Er*Bt4c0v9~*S*aJ<)(b_;vOt$#`pS$+OKIP)r1~g<+XAV(M4z+$4#{O6k-SQ z{(a?OIJPz~oD$UHB=p|mXaZf>qcydox zH^)-+wpD)|FKC9W4y{OPp3is8TzDYfiNbJeS3Q++g5 z?Fl*jxV}?}=I3uQeF~$S`V`beCn_LBur_V)tD0R2XxYN)`|Oc7=3R^HM)h~YRkvP= zMk91Y3eJk*v58Tg2CZ2n1AWNssZp3(zRRIz?kONv%^oJU*i@TeEl_*;DRe`i`+Kh? z!X#y^58}(P?u>uO$Nb9>wk7YtM^&+7EI(Xwx1cV=!;M-vz^z3H*i=%OgnI!FvFO{2G#Ukbm z9oÐ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%0Dh6ux@PHcubBR9XEZ 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

    =) z5Wgb{4|^eRa1mpJ4!5q88S&M@ELQRoiVONTOg>1sxdUXb&gX?go|JppX4IXB|PO8fRGsyw_q&g>$LM~f?rYJI>;aJ z-GR~nwe<0p2PvF7)Mx%&JRXe9@_4Fz5Ge0^<^$y3#ILQ*l53<@yh;H;Gb38kAi|2itES`gRQ^GS)zt%BStVWR}Xy$ni_Z zWI+QOyIiBMf`;lGsG}at0Etgk&r({L>^cShJ6JD(C7h~CE(kPHkBMy@R`~h`HC?K( z1-w)Ow&x83vkp2gG(QzLHL`~obAF{X>bL)N24TY-gUnS}4XvE*x;OpFX`LPyBTVQ| z!EdUHO+U-B?z^q$;`mv2r(lth|JX{C_9Kj2`8?p#rdj}Y%toE~F;yGH^l^fFLA9nQ z36!;RiU_c8XO`$K*@= z$o28s!%BivCJ*lIV@lZ+C?P8S)XlZC})6c_-2AmdUUjk=L#p&=X;U?PnaHetGBuETT9wCjFUas zzws-D{DZh_ZvBGQf%Y!fM&p8z{FU$tL%0AW5XoW^xjA!Yi^t;R{UXK14kzpV8acb= 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 N^sxSK*n{V9{x3QlxlRB8 literal 0 HcmV?d00001 diff --git a/examples/advanced-pytorch/client.py b/examples/advanced-pytorch/client.py deleted file mode 100644 index 1b93d45d950e..000000000000 --- a/examples/advanced-pytorch/client.py +++ /dev/null @@ -1,160 +0,0 @@ -import argparse -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") - - -class CifarClient(fl.client.NumPyClient): - def __init__( - self, - trainset: datasets.Dataset, - testset: datasets.Dataset, - device: torch.device, - model_str: str, - validation_split: int = 0.1, - ): - self.device = device - self.trainset = trainset - self.testset = testset - self.validation_split = validation_split - if model_str == "alexnet": - self.model = utils.load_alexnet(classes=10) - else: - self.model = utils.load_efficientnet(classes=10) - - def set_parameters(self, parameters): - """Loads a alexnet or efficientnet model and replaces it parameters with the - ones given.""" - - params_dict = zip(self.model.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - self.model.load_state_dict(state_dict, strict=True) - - def fit(self, parameters, config): - """Train parameters on the locally held training set.""" - - # Update local model parameters - self.set_parameters(parameters) - - # Get hyperparameters for this round - batch_size: int = config["batch_size"] - epochs: int = config["local_epochs"] - - train_valid = self.trainset.train_test_split(self.validation_split, seed=42) - trainset = train_valid["train"] - valset = train_valid["test"] - - train_loader = DataLoader(trainset, batch_size=batch_size, shuffle=True) - val_loader = DataLoader(valset, batch_size=batch_size) - - results = utils.train(self.model, train_loader, val_loader, epochs, self.device) - - parameters_prime = utils.get_model_params(self.model) - num_examples_train = len(trainset) - - return parameters_prime, num_examples_train, results - - def evaluate(self, parameters, config): - """Evaluate parameters on the locally held test set.""" - # Update local model parameters - self.set_parameters(parameters) - - # Get config values - steps: int = config["val_steps"] - - # Evaluate global model parameters on the local test data and return results - testloader = DataLoader(self.testset, batch_size=16) - - loss, accuracy = utils.test(self.model, testloader, steps, self.device) - return float(loss), len(self.testset), {"accuracy": float(accuracy)} - - -def client_dry_run(device: torch.device = "cpu"): - """Weak tests to check whether all client methods are working as expected.""" - - model = utils.load_efficientnet(classes=10) - trainset, testset = utils.load_partition(0) - trainset = trainset.select(range(10)) - testset = testset.select(range(10)) - client = CifarClient(trainset, testset, device) - client.fit( - utils.get_model_params(model), - {"batch_size": 16, "local_epochs": 1}, - ) - - client.evaluate(utils.get_model_params(model), {"val_steps": 32}) - - print("Dry Run Successful") - - -def main() -> None: - # Parse command line argument `partition` - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--dry", - type=bool, - default=False, - required=False, - help="Do a dry-run to check the client", - ) - parser.add_argument( - "--client-id", - type=int, - default=0, - choices=range(0, 10), - required=False, - help="Specifies the artificial data partition of CIFAR10 to be used. \ - Picks partition 0 by default", - ) - parser.add_argument( - "--toy", - action="store_true", - help="Set to true to quicky run the client using only 10 datasamples. \ - Useful for testing purposes. Default: False", - ) - parser.add_argument( - "--use_cuda", - type=bool, - default=False, - required=False, - help="Set to true to use GPU. Default: False", - ) - parser.add_argument( - "--model", - type=str, - default="efficientnet", - choices=["efficientnet", "alexnet"], - help="Use either Efficientnet or Alexnet models. \ - If you want to achieve differential privacy, please use the Alexnet model", - ) - - args = parser.parse_args() - - device = torch.device( - "cuda:0" if torch.cuda.is_available() and args.use_cuda else "cpu" - ) - - if args.dry: - client_dry_run(device) - else: - # Load a subset of CIFAR-10 to simulate the local data partition - trainset, testset = utils.load_partition(args.client_id) - - if args.toy: - trainset = trainset.select(range(10)) - testset = testset.select(range(10)) - # Start Flower client - client = CifarClient(trainset, testset, device, args.model).to_client() - fl.client.start_client(server_address="127.0.0.1:8080", client=client) - - -if __name__ == "__main__": - main() diff --git a/examples/advanced-pytorch/pyproject.toml b/examples/advanced-pytorch/pyproject.toml index f2c9ad731196..553abeecb6ad 100644 --- a/examples/advanced-pytorch/pyproject.toml +++ b/examples/advanced-pytorch/pyproject.toml @@ -1,20 +1,46 @@ [build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.poetry] -name = "advanced-pytorch" -version = "0.1.0" -description = "Advanced Flower/PyTorch Example" -authors = [ - "The Flower Authors ", - "Kaushik Amar Das ", +[project] +name = "pytorch-example" +version = "1.0.0" +description = "Federated Learning with PyTorch and Flower (Advanced Example)" +license = "Apache-2.0" +dependencies = [ + "flwr[simulation]>=1.11.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", + "wandb==0.17.8", ] -[tool.poetry.dependencies] -python = ">=3.9,<3.11" -flwr = ">=1.0,<2.0" -flwr-datasets = { extras = ["vision"], version = ">=0.0.2,<1.0.0" } -torch = "1.13.1" -torchvision = "0.14.1" -validators = "0.18.2" +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flwrlabs" + +[tool.flwr.app.components] +serverapp = "pytorch_example.server_app:app" +clientapp = "pytorch_example.client_app:app" + +[tool.flwr.app.config] +num-server-rounds = 10 +fraction-fit = 0.25 +fraction-evaluate = 0.5 +local-epochs = 1 +server-device = "cpu" +use-wandb = true + +[tool.flwr.federations] +default = "local-sim" + +[tool.flwr.federations.local-sim] +options.num-supernodes = 50 +options.backend.client-resources.num-cpus = 2 # each ClientApp assumes to use 2CPUs +options.backend.client-resources.num-gpus = 0.0 # ratio of VRAM a ClientApp has access to +[tool.flwr.federations.local-sim-gpu] +options.num-supernodes = 50 +options.backend.client-resources.num-cpus = 2 +options.backend.client-resources.num-gpus = 0.25 diff --git a/examples/advanced-pytorch/pytorch_example/__init__.py b/examples/advanced-pytorch/pytorch_example/__init__.py new file mode 100644 index 000000000000..d93e8cdb922d --- /dev/null +++ b/examples/advanced-pytorch/pytorch_example/__init__.py @@ -0,0 +1 @@ +"""pytorch-example: A Flower / PyTorch app.""" diff --git a/examples/advanced-pytorch/pytorch_example/client_app.py b/examples/advanced-pytorch/pytorch_example/client_app.py new file mode 100644 index 000000000000..72a9c8323686 --- /dev/null +++ b/examples/advanced-pytorch/pytorch_example/client_app.py @@ -0,0 +1,122 @@ +"""pytorch-example: A Flower / PyTorch app.""" + +import torch +from pytorch_example.task import Net, get_weights, load_data, set_weights, test, train + +from flwr.client import ClientApp, NumPyClient +from flwr.common import Context, ParametersRecord, RecordSet, array_from_numpy + + +# Define Flower Client and client_fn +class FlowerClient(NumPyClient): + """A simple client that showcases how to use the state. + + It implements a basic version of `personalization` by which + the classification layer of the CNN is stored locally and used + and updated during `fit()` and used during `evaluate()`. + """ + + def __init__( + self, net, client_state: RecordSet, trainloader, valloader, local_epochs + ): + self.net: Net = net + self.client_state = client_state + 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) + self.local_layer_name = "classification-head" + + def fit(self, parameters, config): + """Train model locally. + + The client stores in its context the parameters of the last layer in the model + (i.e. the classification head). The classifier is saved at the end of the + training and used the next time this client participates. + """ + + # Apply weights from global models (the whole model is replaced) + set_weights(self.net, parameters) + + # Override weights in classification layer with those this client + # had at the end of the last fit() round it participated in + self._load_layer_weights_from_state() + + train_loss = train( + self.net, + self.trainloader, + self.local_epochs, + lr=float(config["lr"]), + device=self.device, + ) + # Save classification head to context's state to use in a future fit() call + self._save_layer_weights_to_state() + + # Return locally-trained model and metrics + return ( + get_weights(self.net), + len(self.trainloader.dataset), + {"train_loss": train_loss}, + ) + + def _save_layer_weights_to_state(self): + """Save last layer weights to state.""" + state_dict_arrays = {} + for k, v in self.net.fc2.state_dict().items(): + state_dict_arrays[k] = array_from_numpy(v.cpu().numpy()) + + # Add to recordset (replace if already exists) + self.client_state.parameters_records[self.local_layer_name] = ParametersRecord( + state_dict_arrays + ) + + def _load_layer_weights_from_state(self): + """Load last layer weights to state.""" + if self.local_layer_name not in self.client_state.parameters_records: + return + + state_dict = {} + param_records = self.client_state.parameters_records + for k, v in param_records[self.local_layer_name].items(): + state_dict[k] = torch.from_numpy(v.numpy()) + + # apply previously saved classification head by this client + self.net.fc2.load_state_dict(state_dict, strict=True) + + def evaluate(self, parameters, config): + """Evaluate the global model on the local validation set. + + Note the classification head is replaced with the weights this client had the + last time it trained the model. + """ + set_weights(self.net, parameters) + # Override weights in classification layer with those this client + # had at the end of the last fit() round it participated in + self._load_layer_weights_from_state() + 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() + 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 + # We pass the state to persist information across + # participation rounds. Note that each client always + # receives the same Context instance (it's a 1:1 mapping) + client_state = context.state + return FlowerClient( + net, client_state, trainloader, valloader, local_epochs + ).to_client() + + +# Flower ClientApp +app = ClientApp( + client_fn, +) diff --git a/examples/advanced-pytorch/pytorch_example/server_app.py b/examples/advanced-pytorch/pytorch_example/server_app.py new file mode 100644 index 000000000000..3fa2ae26dc7f --- /dev/null +++ b/examples/advanced-pytorch/pytorch_example/server_app.py @@ -0,0 +1,96 @@ +"""pytorch-example: A Flower / PyTorch app.""" + +import torch +from pytorch_example.strategy import CustomFedAvg +from pytorch_example.task import ( + Net, + apply_eval_transforms, + get_weights, + set_weights, + test, +) +from torch.utils.data import DataLoader + +from datasets import load_dataset +from flwr.common import Context, ndarrays_to_parameters +from flwr.server import ServerApp, ServerAppComponents, ServerConfig + + +def gen_evaluate_fn( + testloader: DataLoader, + device: torch.device, +): + """Generate the function for centralized evaluation.""" + + def evaluate(server_round, parameters_ndarrays, config): + """Evaluate global model on centralized test set.""" + net = Net() + set_weights(net, parameters_ndarrays) + net.to(device) + loss, accuracy = test(net, testloader, device=device) + return loss, {"centralized_accuracy": accuracy} + + return evaluate + + +def on_fit_config(server_round: int): + """Construct `config` that clients receive when running `fit()`""" + lr = 0.1 + # Enable a simple form of learning rate decay + if server_round > 10: + lr /= 2 + return {"lr": lr} + + +# Define metric aggregation function +def weighted_average(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 {"federated_evaluate_accuracy": sum(accuracies) / sum(examples)} + + +def server_fn(context: Context): + # Read from config + num_rounds = context.run_config["num-server-rounds"] + fraction_fit = context.run_config["fraction-fit"] + fraction_eval = context.run_config["fraction-evaluate"] + server_device = context.run_config["server-device"] + + # Initialize model parameters + ndarrays = get_weights(Net()) + parameters = ndarrays_to_parameters(ndarrays) + + # Prepare dataset for central evaluation + + # This is the exact same dataset as the one donwloaded by the clients via + # FlowerDatasets. However, we don't use FlowerDatasets for the server since + # partitioning is not needed. + # We make use of the "test" split only + global_test_set = load_dataset("zalando-datasets/fashion_mnist")["test"] + + testloader = DataLoader( + global_test_set.with_transform(apply_eval_transforms), + batch_size=32, + ) + + # Define strategy + strategy = CustomFedAvg( + run_config=context.run_config, + use_wandb=context.run_config["use-wandb"], + fraction_fit=fraction_fit, + fraction_evaluate=fraction_eval, + initial_parameters=parameters, + on_fit_config_fn=on_fit_config, + evaluate_fn=gen_evaluate_fn(testloader, device=server_device), + evaluate_metrics_aggregation_fn=weighted_average, + ) + config = ServerConfig(num_rounds=num_rounds) + + return ServerAppComponents(strategy=strategy, config=config) + + +# Create ServerApp +app = ServerApp(server_fn=server_fn) diff --git a/examples/advanced-pytorch/pytorch_example/strategy.py b/examples/advanced-pytorch/pytorch_example/strategy.py new file mode 100644 index 000000000000..97fc0010f143 --- /dev/null +++ b/examples/advanced-pytorch/pytorch_example/strategy.py @@ -0,0 +1,116 @@ +"""pytorch-example: A Flower / PyTorch app.""" + +import json +from logging import INFO + +import torch +import wandb +from pytorch_example.task import Net, create_run_dir, set_weights + +from flwr.common import logger, parameters_to_ndarrays +from flwr.common.typing import UserConfig +from flwr.server.strategy import FedAvg + +PROJECT_NAME = "FLOWER-advanced-pytorch" + + +class CustomFedAvg(FedAvg): + """A class that behaves like FedAvg but has extra functionality. + + This strategy: (1) saves results to the filesystem, (2) saves a + checkpoint of the global model when a new best is found, (3) logs + results to W&B if enabled. + """ + + def __init__(self, run_config: UserConfig, use_wandb: bool, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Create a directory where to save results from this run + self.save_path, self.run_dir = create_run_dir(run_config) + self.use_wandb = use_wandb + # Initialise W&B if set + if use_wandb: + self._init_wandb_project() + + # Keep track of best acc + self.best_acc_so_far = 0.0 + + # A dictionary to store results as they come + self.results = {} + + def _init_wandb_project(self): + # init W&B + wandb.init(project=PROJECT_NAME, name=f"{str(self.run_dir)}-ServerApp") + + def _store_results(self, tag: str, results_dict): + """Store results in dictionary, then save as JSON.""" + # Update results dict + if tag in self.results: + self.results[tag].append(results_dict) + else: + self.results[tag] = [results_dict] + + # Save results to disk. + # Note we overwrite the same file with each call to this function. + # While this works, a more sophisticated approach is preferred + # in situations where the contents to be saved are larger. + with open(f"{self.save_path}/results.json", "w", encoding="utf-8") as fp: + json.dump(self.results, fp) + + def _update_best_acc(self, round, accuracy, parameters): + """Determines if a new best global model has been found. + + If so, the model checkpoint is saved to disk. + """ + if accuracy > self.best_acc_so_far: + self.best_acc_so_far = accuracy + logger.log(INFO, "💡 New best global model found: %f", accuracy) + # You could save the parameters object directly. + # Instead we are going to apply them to a PyTorch + # model and save the state dict. + # Converts flwr.common.Parameters to ndarrays + ndarrays = parameters_to_ndarrays(parameters) + model = Net() + set_weights(model, ndarrays) + # Save the PyTorch model + file_name = f"model_state_acc_{accuracy}_round_{round}.pth" + torch.save(model.state_dict(), self.save_path / file_name) + + def store_results_and_log(self, server_round: int, tag: str, results_dict): + """A helper method that stores results and logs them to W&B if enabled.""" + # Store results + self._store_results( + tag=tag, + results_dict={"round": server_round, **results_dict}, + ) + + if self.use_wandb: + # Log centralized loss and metrics to W&B + wandb.log(results_dict, step=server_round) + + def evaluate(self, server_round, parameters): + """Run centralized evaluation if callback was passed to strategy init.""" + loss, metrics = super().evaluate(server_round, parameters) + + # Save model if new best central accuracy is found + self._update_best_acc(server_round, metrics["centralized_accuracy"], parameters) + + # Store and log + self.store_results_and_log( + server_round=server_round, + tag="centralized_evaluate", + results_dict={"centralized_loss": loss, **metrics}, + ) + return loss, metrics + + def aggregate_evaluate(self, server_round, results, failures): + """Aggregate results from federated evaluation.""" + loss, metrics = super().aggregate_evaluate(server_round, results, failures) + + # Store and log + self.store_results_and_log( + server_round=server_round, + tag="federated_evaluate", + results_dict={"federated_evaluate_loss": loss, **metrics}, + ) + return loss, metrics diff --git a/examples/advanced-pytorch/pytorch_example/task.py b/examples/advanced-pytorch/pytorch_example/task.py new file mode 100644 index 000000000000..0224e8236408 --- /dev/null +++ b/examples/advanced-pytorch/pytorch_example/task.py @@ -0,0 +1,159 @@ +"""pytorch-example: A Flower / PyTorch app.""" + +import json +from collections import OrderedDict +from datetime import datetime +from pathlib import Path + +import torch +import torch.nn as nn +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import DirichletPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import ( + Compose, + Normalize, + RandomCrop, + RandomHorizontalFlip, + ToTensor, +) + +from flwr.common.typing import UserConfig + +FM_NORMALIZATION = ((0.1307,), (0.3081,)) +EVAL_TRANSFORMS = Compose([ToTensor(), Normalize(*FM_NORMALIZATION)]) +TRAIN_TRANSFORMS = Compose( + [ + RandomCrop(28, padding=4), + RandomHorizontalFlip(), + ToTensor(), + Normalize(*FM_NORMALIZATION), + ] +) + + +class Net(nn.Module): + """Model (simple CNN adapted for Fashion-MNIST)""" + + def __init__(self): + super().__init__() + self.conv1 = nn.Conv2d(1, 16, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(16, 32, 5) + self.fc1 = nn.Linear(32 * 4 * 4, 128) + self.fc2 = nn.Linear(128, 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, 32 * 4 * 4) + x = F.relu(self.fc1(x)) + return self.fc2(x) + + +def train(net, trainloader, epochs, lr, 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=lr, momentum=0.9) + net.train() + running_loss = 0.0 + for _ in range(epochs): + for batch in trainloader: + images = batch["image"] + 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["image"].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): + 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) + + +def apply_train_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["image"] = [TRAIN_TRANSFORMS(img) for img in batch["image"]] + return batch + + +def apply_eval_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["image"] = [EVAL_TRANSFORMS(img) for img in batch["image"]] + return batch + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int): + """Load partition FashionMNIST data.""" + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = DirichletPartitioner( + num_partitions=num_partitions, + partition_by="label", + alpha=1.0, + seed=42, + ) + fds = FederatedDataset( + dataset="zalando-datasets/fashion_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) + + train_partition = partition_train_test["train"].with_transform( + apply_train_transforms + ) + test_partition = partition_train_test["test"].with_transform(apply_eval_transforms) + trainloader = DataLoader(train_partition, batch_size=32, shuffle=True) + testloader = DataLoader(test_partition, batch_size=32) + return trainloader, testloader + + +def create_run_dir(config: UserConfig) -> Path: + """Create a directory where to save results from this run.""" + # Create output directory given current timestamp + current_time = datetime.now() + run_dir = current_time.strftime("%Y-%m-%d/%H-%M-%S") + # Save path is based on the current directory + save_path = Path.cwd() / f"outputs/{run_dir}" + save_path.mkdir(parents=True, exist_ok=False) + + # Save run config as json + with open(f"{save_path}/run_config.json", "w", encoding="utf-8") as fp: + json.dump(config, fp) + + return save_path, run_dir diff --git a/examples/advanced-pytorch/requirements.txt b/examples/advanced-pytorch/requirements.txt deleted file mode 100644 index f4d6a0774162..000000000000 --- a/examples/advanced-pytorch/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -flwr>=1.0, <2.0 -flwr-datasets[vision]>=0.0.2, <1.0.0 -torch==1.13.1 -torchvision==0.14.1 -validators==0.18.2 diff --git a/examples/advanced-pytorch/run.sh b/examples/advanced-pytorch/run.sh deleted file mode 100755 index c3d52491b987..000000000000 --- a/examples/advanced-pytorch/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -e -cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/ - -python server.py --toy & -sleep 10 # Sleep for 10s to give the server enough time to start and dowload the dataset - -for i in `seq 0 9`; do - echo "Starting client $i" - python client.py --client-id=${i} --toy & -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/advanced-pytorch/server.py b/examples/advanced-pytorch/server.py deleted file mode 100644 index 6b69512fb3b7..000000000000 --- a/examples/advanced-pytorch/server.py +++ /dev/null @@ -1,121 +0,0 @@ -import argparse -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 - -warnings.filterwarnings("ignore") - - -def fit_config(server_round: int): - """Return training configuration dict for each round. - - Keep batch size fixed at 32, perform two rounds of training with one local epoch, - increase to two local epochs afterwards. - """ - config = { - "batch_size": 16, - "local_epochs": 1 if server_round < 2 else 2, - } - return config - - -def evaluate_config(server_round: int): - """Return evaluation configuration dict for each round. - - Perform five local evaluation steps on each client (i.e., use five batches) during - rounds one to three, then increase to ten local evaluation steps. - """ - val_steps = 5 if server_round < 4 else 10 - return {"val_steps": val_steps} - - -def get_evaluate_fn(model: torch.nn.Module, toy: bool): - """Return an evaluation function for server-side evaluation.""" - - # Load data here to avoid the overhead of doing it in `evaluate` itself - centralized_data = utils.load_centralized_data() - if toy: - # use only 10 samples as validation set - centralized_data = centralized_data.select(range(10)) - - val_loader = DataLoader(centralized_data, batch_size=16) - - # The `evaluate` function will be called after every round - def evaluate( - server_round: int, - parameters: fl.common.NDArrays, - config: Dict[str, fl.common.Scalar], - ) -> Optional[Tuple[float, Dict[str, fl.common.Scalar]]]: - # Update model with the latest 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) - - loss, accuracy = utils.test(model, val_loader) - return loss, {"accuracy": accuracy} - - return evaluate - - -def main(): - """Load model for - 1. server-side parameter initialization - 2. server-side parameter evaluation - """ - - # Parse command line argument `partition` - parser = argparse.ArgumentParser(description="Flower") - parser.add_argument( - "--toy", - action="store_true", - help="Set to true to use only 10 datasamples for validation. \ - Useful for testing purposes. Default: False", - ) - parser.add_argument( - "--model", - type=str, - default="efficientnet", - choices=["efficientnet", "alexnet"], - help="Use either Efficientnet or Alexnet models. \ - If you want to achieve differential privacy, please use the Alexnet model", - ) - - args = parser.parse_args() - - if args.model == "alexnet": - model = utils.load_alexnet(classes=10) - else: - model = utils.load_efficientnet(classes=10) - - model_parameters = [val.cpu().numpy() for _, val in model.state_dict().items()] - - # Create strategy - strategy = fl.server.strategy.FedAvg( - fraction_fit=1.0, - fraction_evaluate=1.0, - min_fit_clients=2, - min_evaluate_clients=2, - min_available_clients=10, - evaluate_fn=get_evaluate_fn(model, args.toy), - on_fit_config_fn=fit_config, - on_evaluate_config_fn=evaluate_config, - initial_parameters=fl.common.ndarrays_to_parameters(model_parameters), - ) - - # Start Flower server for four rounds of federated learning - fl.server.start_server( - server_address="0.0.0.0:8080", - config=fl.server.ServerConfig(num_rounds=4), - strategy=strategy, - ) - - -if __name__ == "__main__": - main() diff --git a/examples/advanced-pytorch/utils.py b/examples/advanced-pytorch/utils.py deleted file mode 100644 index d2b3955c9fde..000000000000 --- a/examples/advanced-pytorch/utils.py +++ /dev/null @@ -1,117 +0,0 @@ -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") - - -def load_partition(partition_id, toy: bool = False): - """Load partition CIFAR10 data.""" - fds = FederatedDataset(dataset="cifar10", partitioners={"train": 10}) - 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) - partition_train_test = partition_train_test.with_transform(apply_transforms) - return partition_train_test["train"], partition_train_test["test"] - - -def load_centralized_data(): - fds = FederatedDataset(dataset="cifar10", partitioners={"train": 10}) - centralized_data = fds.load_split("test") - centralized_data = centralized_data.with_transform(apply_transforms) - return centralized_data - - -def apply_transforms(batch): - """Apply transforms to the partition from FederatedDataset.""" - pytorch_transforms = Compose( - [ - Resize(256), - CenterCrop(224), - ToTensor(), - Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ] - ) - batch["img"] = [pytorch_transforms(img) for img in batch["img"]] - return batch - - -def train( - net, trainloader, valloader, epochs, device: torch.device = torch.device("cpu") -): - """Train the network on the training set.""" - print("Starting training...") - 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, weight_decay=1e-4 - ) - net.train() - for _ in range(epochs): - for batch in trainloader: - images, labels = batch["img"], batch["label"] - images, labels = images.to(device), labels.to(device) - optimizer.zero_grad() - loss = criterion(net(images), labels) - loss.backward() - optimizer.step() - - net.to("cpu") # move model back to CPU - - 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 - - -def test( - net, testloader, steps: int = None, device: torch.device = torch.device("cpu") -): - """Validate the network on the entire test set.""" - print("Starting evalutation...") - net.to(device) # move model to GPU if available - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - net.eval() - with torch.no_grad(): - for batch_idx, batch in enumerate(testloader): - images, labels = batch["img"], batch["label"] - images, labels = images.to(device), labels.to(device) - outputs = net(images) - loss += criterion(outputs, labels).item() - _, predicted = torch.max(outputs.data, 1) - correct += (predicted == labels).sum().item() - if steps is not None and batch_idx == steps: - break - accuracy = correct / len(testloader.dataset) - net.to("cpu") # move model back to CPU - return loss, accuracy - - -def load_efficientnet(classes: int = 10): - """Loads EfficienNetB0 from TorchVision.""" - efficientnet = efficientnet_b0(pretrained=True) - # Re-init output linear layer with the right number of classes - model_classes = efficientnet.classifier[1].in_features - if classes != model_classes: - efficientnet.classifier[1] = torch.nn.Linear(model_classes, classes) - return efficientnet - - -def get_model_params(model): - """Returns a model's parameters.""" - return [val.cpu().numpy() for _, val in model.state_dict().items()] - - -def load_alexnet(classes): - """Load AlexNet model from TorchVision.""" - return AlexNet(num_classes=classes) From b5dc9965ee8a577356caa3cc08b6f89302ad4638 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 19 Sep 2024 10:33:18 +0200 Subject: [PATCH 12/16] refactor(examples) Make `quickstart-huggingface`use a smaller LM (#4206) --- examples/quickstart-huggingface/README.md | 6 +++--- examples/quickstart-huggingface/huggingface_example/task.py | 2 +- examples/quickstart-huggingface/pyproject.toml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/quickstart-huggingface/README.md b/examples/quickstart-huggingface/README.md index ac0acebb9b99..124689441656 100644 --- a/examples/quickstart-huggingface/README.md +++ b/examples/quickstart-huggingface/README.md @@ -8,7 +8,7 @@ framework: [transformers] 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. -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. +In this example, we will federated the training of a [BERT-tiny](https://huggingface.co/prajjwal1/bert-tiny) 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. ## Set up the project @@ -57,7 +57,7 @@ You can run your Flower project in both _simulation_ and _deployment_ mode witho flwr run . ``` -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. +Run the project in the `local-simulation-gpu` federation that gives CPU and GPU resources to each `ClientApp`. By default, at most 4x`ClientApp` (using ~1 GB of VRAM each) will run in parallel in each available GPU. Note you can adjust the degree of paralellism but modifying the `client-resources` specification. ```bash # Run with the `local-simulation-gpu` federation @@ -67,7 +67,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 +flwr run --run-config "num-server-rounds=5 fraction-fit=0.1" ``` > \[!TIP\] diff --git a/examples/quickstart-huggingface/huggingface_example/task.py b/examples/quickstart-huggingface/huggingface_example/task.py index 25304d134a67..1c5b8d087dca 100644 --- a/examples/quickstart-huggingface/huggingface_example/task.py +++ b/examples/quickstart-huggingface/huggingface_example/task.py @@ -40,7 +40,7 @@ def load_data( tokenizer = AutoTokenizer.from_pretrained(model_name, model_max_length=512) def tokenize_function(examples): - return tokenizer(examples["text"], truncation=True) + return tokenizer(examples["text"], truncation=True, add_special_tokens=True) partition_train_test = partition_train_test.map(tokenize_function, batched=True) partition_train_test = partition_train_test.remove_columns("text") diff --git a/examples/quickstart-huggingface/pyproject.toml b/examples/quickstart-huggingface/pyproject.toml index 696f05b33ebf..f479acfa0918 100644 --- a/examples/quickstart-huggingface/pyproject.toml +++ b/examples/quickstart-huggingface/pyproject.toml @@ -33,7 +33,7 @@ clientapp = "huggingface_example.client_app:app" [tool.flwr.app.config] num-server-rounds = 3 -model-name = "distilbert-base-uncased" +model-name = "prajjwal1/bert-tiny" fraction-fit = 0.05 fraction-evaluate = 0.1 @@ -46,4 +46,4 @@ options.num-supernodes = 100 [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 +options.backend.client-resources.num-gpus = 0.25 # at most 4 ClientApp will run in a given GPU (lower it to increase parallelism) From 09c96c196c4bb04f695c4325c6d702cccb992b50 Mon Sep 17 00:00:00 2001 From: "Daniel J. Beutel" Date: Thu, 19 Sep 2024 10:40:58 +0200 Subject: [PATCH 13/16] fix(*:skip) Update .editorconfig (#4238) --- .editorconfig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.editorconfig b/.editorconfig index 321808ebaecf..103fe51237c8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,14 @@ profile = black indent_style = space indent_size = 2 +[*.md] +indent_style = space +indent_size = 2 + [*.yml] indent_style = space indent_size = 2 + +[*.toml] +indent_style = space +indent_size = 4 From a8ec4fa53aadc430c26f2b15f592f0cd970d6163 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 19 Sep 2024 10:54:36 +0200 Subject: [PATCH 14/16] feat(framework:skip) Add gcc in Docker compose setup (#4187) Signed-off-by: Robert Steiner Co-authored-by: Flower <148336023+flwrmachine@users.noreply.github.com> --- .../tutorial-quickstart-docker-compose.rst | 7 ++++ src/docker/complete/compose.yml | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/doc/source/docker/tutorial-quickstart-docker-compose.rst b/doc/source/docker/tutorial-quickstart-docker-compose.rst index 49cef55ec5a2..36d9bd58e29e 100644 --- a/doc/source/docker/tutorial-quickstart-docker-compose.rst +++ b/doc/source/docker/tutorial-quickstart-docker-compose.rst @@ -283,6 +283,13 @@ In ``compose.yml``, add the following: dockerfile_inline: | FROM flwr/clientapp:${FLWR_VERSION:-|stable_flwr_version|} + USER root + RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + USER app + WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ diff --git a/src/docker/complete/compose.yml b/src/docker/complete/compose.yml index 8874dc7f4c53..e1dc2f5ffc56 100644 --- a/src/docker/complete/compose.yml +++ b/src/docker/complete/compose.yml @@ -12,6 +12,14 @@ services: dockerfile_inline: | FROM flwr/superexec:${FLWR_VERSION:-1.11.1} + # gcc is required for the fastai quickstart example + USER root + RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + USER app + WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ @@ -83,6 +91,14 @@ services: dockerfile_inline: | FROM flwr/clientapp:${FLWR_VERSION:-1.11.1} + # gcc is required for the fastai quickstart example + USER root + RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + USER app + WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ @@ -106,6 +122,14 @@ services: dockerfile_inline: | FROM flwr/clientapp:${FLWR_VERSION:-1.11.1} + # gcc is required for the fastai quickstart example + USER root + RUN apt-get update \ + && apt-get -y --no-install-recommends install \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + USER app + WORKDIR /app COPY --chown=app:app pyproject.toml . RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ @@ -131,6 +155,14 @@ services: # dockerfile_inline: | # FROM flwr/clientapp:${FLWR_VERSION:-1.11.1} + # # gcc is required for the fastai quickstart example + # USER root + # RUN apt-get update \ + # && apt-get -y --no-install-recommends install \ + # build-essential \ + # && rm -rf /var/lib/apt/lists/* + # USER app + # WORKDIR /app # COPY --chown=app:app pyproject.toml . # RUN sed -i 's/.*flwr\[simulation\].*//' pyproject.toml \ From e49e837ebaceb546dbc2c84b27a543c647ea2155 Mon Sep 17 00:00:00 2001 From: Mohammad Naseri Date: Thu, 19 Sep 2024 10:02:03 +0100 Subject: [PATCH 15/16] refactor(framework) Add further unit tests to sint64 to uint64 conversion utils (#4237) --- .../flwr/server/superlink/state/utils_test.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/py/flwr/server/superlink/state/utils_test.py b/src/py/flwr/server/superlink/state/utils_test.py index 93e678ddd38a..d55e2ffd9aa3 100644 --- a/src/py/flwr/server/superlink/state/utils_test.py +++ b/src/py/flwr/server/superlink/state/utils_test.py @@ -20,7 +20,9 @@ from .utils import ( convert_sint64_to_uint64, + convert_sint64_values_in_dict_to_uint64, convert_uint64_to_sint64, + convert_uint64_values_in_dict_to_sint64, generate_rand_int_from_bytes, ) @@ -84,6 +86,62 @@ def test_uint64_to_sint64_to_uint64(self, expected: int) -> None: actual = convert_sint64_to_uint64(convert_uint64_to_sint64(expected)) self.assertEqual(expected, actual) + @parameterized.expand( # type: ignore + [ + # Test cases with uint64 values + ( + {"a": 0, "b": 2**63 - 1, "c": 2**63, "d": 2**64 - 1}, + ["a", "b", "c", "d"], + {"a": 0, "b": 2**63 - 1, "c": -(2**63), "d": -1}, + ), + ( + {"a": 1, "b": 2**62, "c": 2**63 + 1}, + ["a", "b", "c"], + {"a": 1, "b": 2**62, "c": -(2**63) + 1}, + ), + # Edge cases with mixed uint64 values and keys + ( + {"a": 2**64 - 1, "b": 12345, "c": 0}, + ["a", "b"], + {"a": -1, "b": 12345, "c": 0}, + ), + ] + ) + def test_convert_uint64_values_in_dict_to_sint64( + self, input_dict: dict[str, int], keys: list[str], expected_dict: dict[str, int] + ) -> None: + """Test uint64 to sint64 conversion in a dictionary.""" + convert_uint64_values_in_dict_to_sint64(input_dict, keys) + self.assertEqual(input_dict, expected_dict) + + @parameterized.expand( # type: ignore + [ + # Test cases with sint64 values + ( + {"a": 0, "b": 2**63 - 1, "c": -(2**63), "d": -1}, + ["a", "b", "c", "d"], + {"a": 0, "b": 2**63 - 1, "c": 2**63, "d": 2**64 - 1}, + ), + ( + {"a": -1, "b": -(2**63) + 1, "c": 12345}, + ["a", "b", "c"], + {"a": 2**64 - 1, "b": 2**63 + 1, "c": 12345}, + ), + # Edge cases with mixed sint64 values and keys + ( + {"a": -1, "b": 12345, "c": 0}, + ["a", "b"], + {"a": 2**64 - 1, "b": 12345, "c": 0}, + ), + ] + ) + def test_convert_sint64_values_in_dict_to_uint64( + self, input_dict: dict[str, int], keys: list[str], expected_dict: dict[str, int] + ) -> None: + """Test sint64 to uint64 conversion in a dictionary.""" + convert_sint64_values_in_dict_to_uint64(input_dict, keys) + self.assertEqual(input_dict, expected_dict) + def test_generate_rand_int_from_bytes_unsigned_int(self) -> None: """Test that the generated integer is unsigned (non-negative).""" for num_bytes in range(1, 9): From efdb900671d84cc189af489bea3ac04006fa15d7 Mon Sep 17 00:00:00 2001 From: Robert Steiner Date: Thu, 19 Sep 2024 11:09:38 +0200 Subject: [PATCH 16/16] docs(framework) Add quickstart examples Docker compose guide (#4189) Signed-off-by: Robert Steiner Co-authored-by: Javier --- doc/source/docker/index.rst | 1 + ...run-quickstart-examples-docker-compose.rst | 122 ++++++++++++++++++ .../tutorial-quickstart-docker-compose.rst | 5 + 3 files changed, 128 insertions(+) create mode 100644 doc/source/docker/run-quickstart-examples-docker-compose.rst diff --git a/doc/source/docker/index.rst b/doc/source/docker/index.rst index ac6124b4c138..968f01581b34 100644 --- a/doc/source/docker/index.rst +++ b/doc/source/docker/index.rst @@ -44,3 +44,4 @@ Run Flower using Docker Compose :maxdepth: 1 tutorial-quickstart-docker-compose + run-quickstart-examples-docker-compose diff --git a/doc/source/docker/run-quickstart-examples-docker-compose.rst b/doc/source/docker/run-quickstart-examples-docker-compose.rst new file mode 100644 index 000000000000..b279fb66c45b --- /dev/null +++ b/doc/source/docker/run-quickstart-examples-docker-compose.rst @@ -0,0 +1,122 @@ +Run Flower Quickstart Examples with Docker Compose +================================================== + +Flower provides a set of `quickstart examples `_ +to help you get started with the framework. These examples are designed to demonstrate the +capabilities of Flower and by default run using the Simulation Engine. This guide demonstrates +how to run them using Flower's Deployment Engine via Docker Compose. + +.. important:: + + Some quickstart examples may have limitations or requirements that prevent them from running + on every environment. For more information, please see `Limitations`_. + +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 `_. + +Run the Quickstart Example +-------------------------- + +#. Clone the quickstart example you like to run. For example, ``quickstart-pytorch``: + + .. code-block:: bash + + $ git clone --depth=1 https://github.com/adap/flower.git \ + && mv flower/examples/quickstart-pytorch . \ + && rm -rf flower && cd quickstart-pytorch + +#. Download the `compose.yml `_ file into the example directory: + + .. code-block:: bash + + $ curl https://raw.githubusercontent.com/adap/flower/refs/heads/main/src/docker/complete/compose.yml \ + -o compose.yml + +#. Build and start the services using the following command: + + .. code-block:: bash + + $ docker compose up --build -d + +#. Append the following lines to the end of the ``pyproject.toml`` file and save it: + + .. code-block:: toml + :caption: pyproject.toml + + [tool.flwr.federations.local-deployment] + address = "127.0.0.1:9093" + insecure = true + + .. note:: + + You can customize the string that follows ``tool.flwr.federations.`` to fit your needs. + However, please note that the string cannot contain a dot (``.``). + + In this example, ``local-deployment`` has been used. Just remember to replace + ``local-deployment`` with your chosen name in both the ``tool.flwr.federations.`` string + and the corresponding ``flwr run .`` command. + +#. Run the example: + + .. code-block:: bash + + $ flwr run . local-deployment + +#. Follow the logs of the SuperExec service: + + .. code-block:: bash + + $ docker compose logs superexec -f + +That is all it takes! You can monitor the progress of the run through the logs of the SuperExec. + +Run a Different Quickstart Example +---------------------------------- + +To run a different quickstart example, such as ``quickstart-tensorflow``, first, shut down the Docker +Compose services of the current example: + +.. code-block:: bash + + $ docker compose down + +After that, you can repeat the steps above. + +Limitations +----------- + +.. list-table:: + :header-rows: 1 + + * - Quickstart Example + - Limitations + * - quickstart-fastai + - None + * - examples/quickstart-huggingface + - For CPU-only environments, it requires at least 32GB of memory. + * - quickstart-jax + - The example has not yet been updated to work with the latest ``flwr`` version. + * - quickstart-mlcube + - The example has not yet been updated to work with the latest ``flwr`` version. + * - quickstart-mlx + - `Requires to run on macOS with Apple Silicon `_. + * - quickstart-monai + - None + * - quickstart-pandas + - The example has not yet been updated to work with the latest ``flwr`` version. + * - quickstart-pytorch-lightning + - Requires an older pip version that is not supported by the Flower Docker images. + * - quickstart-pytorch + - None + * - quickstart-sklearn-tabular + - None + * - quickstart-tabnet + - The example has not yet been updated to work with the latest ``flwr`` version. + * - quickstart-tensorflow + - Only runs on AMD64. diff --git a/doc/source/docker/tutorial-quickstart-docker-compose.rst b/doc/source/docker/tutorial-quickstart-docker-compose.rst index 36d9bd58e29e..7aeae1e2fb6b 100644 --- a/doc/source/docker/tutorial-quickstart-docker-compose.rst +++ b/doc/source/docker/tutorial-quickstart-docker-compose.rst @@ -396,3 +396,8 @@ Remove all services and volumes: $ docker compose down -v $ docker compose -f certs.yml down -v + +Where to Go Next +---------------- + +* :doc:`run-quickstart-examples-docker-compose`