From 7101d3b843f2b9d531c49b99cf140ffa38100d99 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Wed, 17 Jul 2024 13:28:52 +0200 Subject: [PATCH 01/36] Update secure aggregation example --- examples/app-secure-aggregation/README.md | 99 ------------------- .../app-secure-aggregation/pyproject.toml | 14 --- .../app-secure-aggregation/requirements.txt | 1 - examples/app-secure-aggregation/run.sh | 34 ------- .../flower-secure-aggregation/pyproject.toml | 33 +++++++ .../secaggexample/__init__.py | 1 + .../secaggexample/client_app.py} | 12 ++- .../secaggexample/server_app.py} | 3 +- .../secaggexample}/workflow_with_log.py | 0 9 files changed, 45 insertions(+), 152 deletions(-) delete mode 100644 examples/app-secure-aggregation/README.md delete mode 100644 examples/app-secure-aggregation/pyproject.toml delete mode 100644 examples/app-secure-aggregation/requirements.txt delete mode 100755 examples/app-secure-aggregation/run.sh create mode 100644 examples/flower-secure-aggregation/pyproject.toml create mode 100644 examples/flower-secure-aggregation/secaggexample/__init__.py rename examples/{app-secure-aggregation/client.py => flower-secure-aggregation/secaggexample/client_app.py} (65%) rename examples/{app-secure-aggregation/server.py => flower-secure-aggregation/secaggexample/server_app.py} (91%) rename examples/{app-secure-aggregation => flower-secure-aggregation/secaggexample}/workflow_with_log.py (100%) diff --git a/examples/app-secure-aggregation/README.md b/examples/app-secure-aggregation/README.md deleted file mode 100644 index 8e483fb2f6b..00000000000 --- a/examples/app-secure-aggregation/README.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -tags: [basic, vision, fds] -dataset: [] -framework: [numpy] ---- - -# Secure aggregation with Flower (the SecAgg+ protocol) ๐Ÿงช - -> ๐Ÿงช = This example covers experimental features that might change in future versions of Flower -> Please consult the regular PyTorch code examples ([quickstart](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch), [advanced](https://github.com/adap/flower/tree/main/examples/advanced-pytorch)) to learn how to use Flower with PyTorch. - -The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflow`. - -## Preconditions - -Let's assume the following project structure: - -```bash -$ tree . -. -โ”œโ”€โ”€ client.py # Client application using `secaggplus_mod` -โ”œโ”€โ”€ server.py # Server application using `SecAggPlusWorkflow` -โ”œโ”€โ”€ workflow_with_log.py # Augmented `SecAggPlusWorkflow` -โ”œโ”€โ”€ run.sh # Quick start script -โ”œโ”€โ”€ pyproject.toml # Project dependencies (poetry) -โ””โ”€โ”€ requirements.txt # Project dependencies (pip) -``` - -## Installing dependencies - -Project dependencies (such as and `flwr`) are defined in `pyproject.toml`. We recommend [Poetry](https://python-poetry.org/docs/) to install those dependencies and manage your virtual environment ([Poetry installation](https://python-poetry.org/docs/#installation)), but feel free to use a different way of installing dependencies and managing virtual environments if you have other preferences. - -### Poetry - -```shell -poetry install -poetry shell -``` - -Poetry will install all your dependencies in a newly created virtual environment. To verify that everything works correctly you can run the following command: - -```shell -poetry run python3 -c "import flwr" -``` - -### pip - -Write the command below in your terminal to install the dependencies according to the configuration file requirements.txt. - -```shell -pip install -r requirements.txt -``` - -If you don't see any errors you're good to go! - -## Run the example with one command (recommended) - -```bash -./run.sh -``` - -## Run the example with the simulation engine - -```bash -flower-simulation --server-app server:app --client-app client:app --num-supernodes 5 -``` - -## Alternatively, run the example (in 7 terminal windows) - -Start the Flower Superlink in one terminal window: - -```bash -flower-superlink --insecure -``` - -Start 5 Flower `ClientApp` in 5 separate terminal windows: - -```bash -flower-client-app client:app --insecure -``` - -Start the Flower `ServerApp`: - -```bash -flower-server-app server:app --insecure --verbose -``` - -## Amend the example for practical usage - -For real-world applications, modify the `workflow` in `server.py` as follows: - -```python -workflow = fl.server.workflow.DefaultWorkflow( - fit_workflow=SecAggPlusWorkflow( - num_shares=, - reconstruction_threshold=, - ) -) -``` diff --git a/examples/app-secure-aggregation/pyproject.toml b/examples/app-secure-aggregation/pyproject.toml deleted file mode 100644 index fb1f636d8c3..00000000000 --- a/examples/app-secure-aggregation/pyproject.toml +++ /dev/null @@ -1,14 +0,0 @@ -[build-system] -requires = ["poetry-core>=1.4.0"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "app-secure-aggregation" -version = "0.1.0" -description = "Flower Secure Aggregation example." -authors = ["The Flower Authors "] - -[tool.poetry.dependencies] -python = "^3.8" -# Mandatory dependencies -flwr = { version = "^1.8.0", extras = ["simulation"] } diff --git a/examples/app-secure-aggregation/requirements.txt b/examples/app-secure-aggregation/requirements.txt deleted file mode 100644 index 2d8be098f26..00000000000 --- a/examples/app-secure-aggregation/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -flwr[simulation]>=1.8.0 diff --git a/examples/app-secure-aggregation/run.sh b/examples/app-secure-aggregation/run.sh deleted file mode 100755 index fa8dc47f26e..00000000000 --- a/examples/app-secure-aggregation/run.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Kill any currently running client.py processes -pkill -f 'flower-client-app' - -# Kill any currently running flower-superlink processes -pkill -f 'flower-superlink' - -# Start the flower server -echo "Starting flower server in background..." -flower-superlink --insecure > /dev/null 2>&1 & -sleep 2 - -# Number of client processes to start -N=5 # Replace with your desired value - -echo "Starting $N ClientApps in background..." - -# Start N client processes -for i in $(seq 1 $N) -do - flower-client-app --insecure client:app > /dev/null 2>&1 & - sleep 0.1 -done - -echo "Starting ServerApp..." -flower-server-app --insecure server:app --verbose - -echo "Clearing background processes..." - -# Kill any currently running client.py processes -pkill -f 'flower-client-app' - -# Kill any currently running flower-superlink processes -pkill -f 'flower-superlink' diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml new file mode 100644 index 00000000000..3bdb9f30bac --- /dev/null +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "secaggexample" +version = "1.0.0" +description = "" +license = { text = "Apache License (2.0)" } +dependencies = [ + "flwr[simulation]>=1.9.0,<2.0", + "numpy==1.24.4", +] + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.flwr.app] +publisher = "flowerteam" + +[tool.flwr.app.components] +serverapp = "secaggexample.server_app:app" +clientapp = "secaggexample.client_app:app" + + +[tool.flwr.app.config] +num_server_rounds = "3" + +[tool.flwr.federations] +default = "localhost" + +[tool.flwr.federations.localhost] +options.num-supernodes = 10 diff --git a/examples/flower-secure-aggregation/secaggexample/__init__.py b/examples/flower-secure-aggregation/secaggexample/__init__.py new file mode 100644 index 00000000000..beb5becac69 --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/__init__.py @@ -0,0 +1 @@ +"""secaggexample.""" diff --git a/examples/app-secure-aggregation/client.py b/examples/flower-secure-aggregation/secaggexample/client_app.py similarity index 65% rename from examples/app-secure-aggregation/client.py rename to examples/flower-secure-aggregation/secaggexample/client_app.py index b2fd02ec00d..006a698119a 100644 --- a/examples/app-secure-aggregation/client.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -1,6 +1,7 @@ import time -from flwr.client import ClientApp, NumPyClient +from flwr.common import Context +from flwr.client import ClientApp, NumPyClient, Client from flwr.client.mod import secaggplus_mod import numpy as np @@ -20,8 +21,13 @@ def fit(self, parameters, config): return ret_vec, 1, {} -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" +def client_fn(context: Context) -> Client: + """Construct a Client that will be run in a ClientApp. + + You can use settings in `context.run_config` to parameterize the + construction of your Client. You could use the `context.node_config` to + , for example, indicate which dataset to load (e.g accesing the partition-id). + """ return FlowerClient().to_client() diff --git a/examples/app-secure-aggregation/server.py b/examples/flower-secure-aggregation/secaggexample/server_app.py similarity index 91% rename from examples/app-secure-aggregation/server.py rename to examples/flower-secure-aggregation/secaggexample/server_app.py index e9737a5a3c7..4b7827f6c1f 100644 --- a/examples/app-secure-aggregation/server.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -21,9 +21,10 @@ @app.main() def main(driver: Driver, context: Context) -> None: # Construct the LegacyContext + num_rounds = int(context.run_config["num_server_rounds"]) context = LegacyContext( state=context.state, - config=ServerConfig(num_rounds=3), + config=ServerConfig(num_rounds=num_rounds), strategy=strategy, ) diff --git a/examples/app-secure-aggregation/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py similarity index 100% rename from examples/app-secure-aggregation/workflow_with_log.py rename to examples/flower-secure-aggregation/secaggexample/workflow_with_log.py From 0d5f436ceb6b871288020af955d9e97dc8e1e8ad Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Wed, 17 Jul 2024 13:29:44 +0200 Subject: [PATCH 02/36] Update secure aggregation example --- examples/flower-secure-aggregation/README.md | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/flower-secure-aggregation/README.md diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md new file mode 100644 index 00000000000..613e2ca2706 --- /dev/null +++ b/examples/flower-secure-aggregation/README.md @@ -0,0 +1,82 @@ +--- +title: Simple Secure Aggregation with Flower Example +tags: [basic, vision, fds] +dataset: [] +framework: [numpy] +--- + +# Secure aggregation with Flower (the SecAgg+ protocol) + +The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflow`. + +## Project Setup + +Start by cloning the example project: + +```shell +git clone --depth=1 https://github.com/adap/flower.git _tmp \ + && mv _tmp/examples/flower-secure-aggregation . \ + && rm -rf _tmp && cd flower-secure-aggregation +``` + +This will create a new directory called `flower-secure-aggregation` containing the +following files: + +```shell +flower-secure-aggregation + | + โ”œโ”€โ”€ secaggexample + | โ”œโ”€โ”€ __init__.py + | โ”œโ”€โ”€ client_app.py # defines your ClientApp + | โ”œโ”€โ”€ server_app.py # defines your ServerApp + | โ””โ”€โ”€ task.py + โ”œโ”€โ”€ pyproject.toml # builds your FAB, includes dependencies and configs + โ””โ”€โ”€ README.md +``` + +## Install dependencies + +```bash +pip install . +``` + +## Run the Example + +You can run your `ClientApp` and `ServerApp` in both _simulation_ and +_deployment_ mode without making changes to the code. If you are starting +with Flower, we recommend you using the _simulation_ model as it requires +fewer components to be launched manually. By default, `flwr run` will make +use of the Simluation Engine. Refer to alternative ways of running your +Flower application including Deployment, with TLS certificates, or with +Docker later in this readme. + +### Run with the Simulation Engine + +Run: + +```bash +flwr run +``` + +You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example + +```bash +flwr run --run-config 'num_server_rounds=5' +``` + +### Alternative wasy of running the example + +TODO: point to docs + +## Amend the example for practical usage + +For real-world applications, modify the `workflow` in `secaggexample/server_app.py` as follows: + +```python +workflow = fl.server.workflow.DefaultWorkflow( + fit_workflow=SecAggPlusWorkflow( + num_shares=, + reconstruction_threshold=, + ) +) +``` From ed3d590f817f4216547259424e1a28735dfeb2a5 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 19 Jul 2024 13:39:07 +0100 Subject: [PATCH 03/36] fix bugs --- examples/flower-secure-aggregation/README.md | 4 ++-- .../flower-secure-aggregation/pyproject.toml | 5 ++++- .../secaggexample/client_app.py | 7 ++++--- .../secaggexample/server_app.py | 20 ++++++++++--------- .../secaggexample/workflow_with_log.py | 12 ++++++----- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 613e2ca2706..e7a01b3c559 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -1,6 +1,6 @@ --- title: Simple Secure Aggregation with Flower Example -tags: [basic, vision, fds] +tags: [basic, secure_aggregation, privacy] dataset: [] framework: [numpy] --- @@ -29,7 +29,7 @@ flower-secure-aggregation | โ”œโ”€โ”€ __init__.py | โ”œโ”€โ”€ client_app.py # defines your ClientApp | โ”œโ”€โ”€ server_app.py # defines your ServerApp - | โ””โ”€โ”€ task.py + | โ””โ”€โ”€ workflow_with_log.py โ”œโ”€โ”€ pyproject.toml # builds your FAB, includes dependencies and configs โ””โ”€โ”€ README.md ``` diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index 3bdb9f30bac..7bad256ac0f 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -25,9 +25,12 @@ clientapp = "secaggexample.client_app:app" [tool.flwr.app.config] num_server_rounds = "3" +num_shares = "3" +reconstruction_threshold = "2" +timeout = "5" [tool.flwr.federations] default = "localhost" [tool.flwr.federations.localhost] -options.num-supernodes = 10 +options.num-supernodes = 5 diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index 006a698119a..f4faa3485ab 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -1,10 +1,11 @@ import time -from flwr.common import Context -from flwr.client import ClientApp, NumPyClient, Client -from flwr.client.mod import secaggplus_mod import numpy as np +from flwr.client import Client, ClientApp, NumPyClient +from flwr.client.mod import secaggplus_mod +from flwr.common import Context + # Define FlowerClient and client_fn class FlowerClient(NumPyClient): diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 4b7827f6c1f..eee2ab4987f 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -1,11 +1,10 @@ +from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs + from flwr.common import Context from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig from flwr.server.strategy import FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow -from workflow_with_log import SecAggPlusWorkflowWithLogs - - # Define strategy strategy = FedAvg( fraction_fit=1.0, # Select all available clients @@ -21,24 +20,27 @@ @app.main() def main(driver: Driver, context: Context) -> None: # Construct the LegacyContext + print(context.run_config) num_rounds = int(context.run_config["num_server_rounds"]) context = LegacyContext( - state=context.state, config=ServerConfig(num_rounds=num_rounds), strategy=strategy, + **vars(context), ) # Create the workflow workflow = DefaultWorkflow( fit_workflow=SecAggPlusWorkflowWithLogs( - num_shares=3, - reconstruction_threshold=2, - timeout=5, + num_shares=int(context.run_config["num_shares"]), + reconstruction_threshold=int( + context.run_config["reconstruction_threshold"] + ), + timeout=float(context.run_config["timeout"]), ) # # For real-world applications, use the following code instead # fit_workflow=SecAggPlusWorkflow( - # num_shares=, - # reconstruction_threshold=, + # num_shares=int(context.run_config["num_shares"]), + # reconstruction_threshold=int(context.run_config["reconstruction_threshold"]), # ) ) diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index a03ff8c13b6..8e137f0beff 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,14 +1,16 @@ -from flwr.common import Context, log, parameters_to_ndarrays from logging import INFO + +import numpy as np + +import flwr.common.recordset_compat as compat +from flwr.common import Context, log, parameters_to_ndarrays +from flwr.common.secure_aggregation.quantization import quantize from flwr.server import Driver, LegacyContext +from flwr.server.workflow.constant import MAIN_PARAMS_RECORD from flwr.server.workflow.secure_aggregation.secaggplus_workflow import ( SecAggPlusWorkflow, WorkflowState, ) -import numpy as np -from flwr.common.secure_aggregation.quantization import quantize -from flwr.server.workflow.constant import MAIN_PARAMS_RECORD -import flwr.common.recordset_compat as compat class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): From 3d74f8cbfa3c270694793b2efbd30f4e5880cd70 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 19 Jul 2024 13:55:12 +0100 Subject: [PATCH 04/36] update legacy_context --- src/py/flwr/server/compat/legacy_context.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/py/flwr/server/compat/legacy_context.py b/src/py/flwr/server/compat/legacy_context.py index ee09d79012d..2e99967e623 100644 --- a/src/py/flwr/server/compat/legacy_context.py +++ b/src/py/flwr/server/compat/legacy_context.py @@ -16,7 +16,7 @@ from dataclasses import dataclass -from typing import Optional +from typing import Dict, Optional from flwr.common import Context, RecordSet @@ -35,9 +35,12 @@ class LegacyContext(Context): client_manager: ClientManager history: History - def __init__( + def __init__( # pylint: disable=too-many-arguments self, - state: RecordSet, + node_id: int = 0, + node_config: Optional[Dict[str, str]] = None, + state: Optional[RecordSet] = None, + run_config: Optional[Dict[str, str]] = None, config: Optional[ServerConfig] = None, strategy: Optional[Strategy] = None, client_manager: Optional[ClientManager] = None, @@ -52,4 +55,10 @@ def __init__( self.strategy = strategy self.client_manager = client_manager self.history = History() - super().__init__(node_id=0, node_config={}, state=state, run_config={}) + + super().__init__( + node_id=node_id, + node_config=node_config if node_config else {}, + state=state if state else RecordSet(), + run_config=run_config if run_config else {}, + ) From dc1bb3e3345c91dab9550fba0152aa74507aa0e2 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Fri, 19 Jul 2024 15:15:29 +0100 Subject: [PATCH 05/36] update readme --- examples/flower-secure-aggregation/README.md | 2 +- examples/flower-secure-aggregation/secaggexample/server_app.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index e7a01b3c559..0d9577c077f 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -7,7 +7,7 @@ framework: [numpy] # Secure aggregation with Flower (the SecAgg+ protocol) -The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflow`. +The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflowWithLogs`, which is a subclass of `SecAggPlusWorkflow` that includes more detailed logging specifically designed for this example. ## Project Setup diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index eee2ab4987f..89d552be464 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -20,7 +20,6 @@ @app.main() def main(driver: Driver, context: Context) -> None: # Construct the LegacyContext - print(context.run_config) num_rounds = int(context.run_config["num_server_rounds"]) context = LegacyContext( config=ServerConfig(num_rounds=num_rounds), From b1db600072c1b053020778a710557c74861d3932 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Mon, 22 Jul 2024 14:16:30 +0100 Subject: [PATCH 06/36] use num_server_rounds from the run_config --- examples/flower-secure-aggregation/secaggexample/server_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 0b248fe00b6..254cc5fbeba 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -23,7 +23,7 @@ def main(driver: Driver, context: Context) -> None: num_rounds = int(context.run_config["num_server_rounds"]) context = LegacyContext( context=context, - config=ServerConfig(num_rounds=3), + config=ServerConfig(num_rounds=num_rounds), strategy=strategy, ) From 73612501bdc982252e18329b09fd5a09f276be32 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Mon, 22 Jul 2024 15:30:01 +0100 Subject: [PATCH 07/36] set log level to DEBUG --- .../secaggexample/workflow_with_log.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index 8e137f0beff..51c345dbe3d 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,9 +1,10 @@ -from logging import INFO +from logging import DEBUG, INFO import numpy as np import flwr.common.recordset_compat as compat from flwr.common import Context, log, parameters_to_ndarrays +from flwr.common.logger import update_console_handler from flwr.common.secure_aggregation.quantization import quantize from flwr.server import Driver, LegacyContext from flwr.server.workflow.constant import MAIN_PARAMS_RECORD @@ -12,6 +13,8 @@ WorkflowState, ) +update_console_handler(DEBUG, True, True) + class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): """The SecAggPlusWorkflow augmented for this example. From 22efc926accb4078292488f1f355f385e02b479d Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha Date: Fri, 26 Jul 2024 20:49:06 +0200 Subject: [PATCH 08/36] Add note --- examples/flower-secure-aggregation/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 0d9577c077f..304a0163558 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -64,9 +64,10 @@ You can also override some of the settings for your `ClientApp` and `ServerApp` flwr run --run-config 'num_server_rounds=5' ``` -### Alternative wasy of running the example +### Run with the Deployment Engine -TODO: point to docs +> \[!NOTE\] +> An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. ## Amend the example for practical usage From ed24cb1d00b32bd294096d72b9eb1f7fc63850cf Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 6 Aug 2024 19:02:45 +0100 Subject: [PATCH 09/36] use real datasets/models for the SA example --- examples/flower-secure-aggregation/README.md | 14 +- .../flower-secure-aggregation/pyproject.toml | 25 ++-- .../secaggexample/client_app.py | 63 +++++--- .../secaggexample/server_app.py | 65 ++++++--- .../secaggexample/task.py | 134 ++++++++++++++++++ .../secaggexample/workflow_with_log.py | 25 ++-- 6 files changed, 260 insertions(+), 66 deletions(-) create mode 100644 examples/flower-secure-aggregation/secaggexample/task.py diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 304a0163558..23038e40e4e 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -71,13 +71,7 @@ flwr run --run-config 'num_server_rounds=5' ## Amend the example for practical usage -For real-world applications, modify the `workflow` in `secaggexample/server_app.py` as follows: - -```python -workflow = fl.server.workflow.DefaultWorkflow( - fit_workflow=SecAggPlusWorkflow( - num_shares=, - reconstruction_threshold=, - ) -) -``` +To adapt the example for real-world applications, follow these steps: + +1. Set `IS_DEMO` to `False` in `./secaggexample/task.py`. +2. Adjust the `num-shares` and `reconstruction-threshold` settings in `./pyproject.toml` to suit your requirements. \ No newline at end of file diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index 7bad256ac0f..c83363d1702 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -5,11 +5,13 @@ build-backend = "hatchling.build" [project] name = "secaggexample" version = "1.0.0" -description = "" +description = "Secure Aggregation in Flower" license = { text = "Apache License (2.0)" } dependencies = [ - "flwr[simulation]>=1.9.0,<2.0", - "numpy==1.24.4", + "flwr[simulation]>=1.10.0", + "flwr-datasets[vision]>=0.3.0", + "torch==2.2.1", + "torchvision==0.17.1", ] [tool.hatch.build.targets.wheel] @@ -24,13 +26,18 @@ clientapp = "secaggexample.client_app:app" [tool.flwr.app.config] -num_server_rounds = "3" -num_shares = "3" -reconstruction_threshold = "2" -timeout = "5" +num-server-rounds = 3 +fraction-evaluate = 0.5 +local-epochs = 1 +learning-rate = 0.1 +batch-size = 32 +# parameters for the SecAgg+ protocol +num-shares = 3 +reconstruction-threshold = 2 +timeout = 15.0 [tool.flwr.federations] -default = "localhost" +default = "local-simulation" -[tool.flwr.federations.localhost] +[tool.flwr.federations.local-simulation] options.num-supernodes = 5 diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index f4faa3485ab..5072a2a9cc7 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -1,35 +1,66 @@ import time -import numpy as np +import torch +from secaggexample.task import Net, get_weights, load_data, set_weights, test, train -from flwr.client import Client, ClientApp, NumPyClient +from flwr.client import ClientApp, NumPyClient from flwr.client.mod import secaggplus_mod from flwr.common import Context -# Define FlowerClient and client_fn +# Define Flower Client class FlowerClient(NumPyClient): + def __init__(self, trainloader, valloader, local_epochs, learning_rate): + self.net = Net() + self.trainloader = trainloader + self.valloader = valloader + self.local_epochs = local_epochs + self.lr = learning_rate + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + def fit(self, parameters, config): - # Instead of training and returning model parameters, - # the client directly returns [1.0, 1.0, 1.0] for demonstration purposes. - ret_vec = [np.ones(3)] + """Train the model with data of this client.""" + set_weights(self.net, parameters) + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.lr, + self.device, + ) + ret_vec = get_weights(self.net) + # Force a significant delay for testing purposes if "drop" in config and config["drop"]: print(f"Client dropped for testing purposes.") - time.sleep(8) + time.sleep(15) else: - print(f"Client uploading {ret_vec[0]}...") - return ret_vec, 1, {} + print(f"Client uploading parameters: {ret_vec[0].flatten()[:3]}...") + return ret_vec, len(self.trainloader.dataset), results + + def evaluate(self, parameters, config): + """Evaluate the model on the data this client has.""" + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.valloader, self.device) + return loss, len(self.valloader.dataset), {"accuracy": accuracy} + + +def client_fn(context: Context): + """Construct a Client that will be run in a ClientApp.""" + # Read the node_config to fetch data partition associated to this node + partition_id = context.node_config["partition-id"] + num_partitions = context.node_config["num-partitions"] -def client_fn(context: Context) -> Client: - """Construct a Client that will be run in a ClientApp. + # Read run_config to fetch hyperparameters relevant to this run + batch_size = context.run_config["batch-size"] + trainloader, valloader = load_data(partition_id, num_partitions, batch_size) + local_epochs = context.run_config["local-epochs"] + learning_rate = context.run_config["learning-rate"] - You can use settings in `context.run_config` to parameterize the - construction of your Client. You could use the `context.node_config` to - , for example, indicate which dataset to load (e.g accesing the partition-id). - """ - return FlowerClient().to_client() + # Return Client instance + return FlowerClient(trainloader, valloader, local_epochs, learning_rate).to_client() # Flower ClientApp diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 254cc5fbeba..179bb6d87f9 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -1,16 +1,26 @@ +from typing import List, Tuple + +from secaggexample.task import IS_DEMO, Net, get_weights from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs -from flwr.common import Context +from flwr.common import Context, Metrics, ndarrays_to_parameters from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig from flwr.server.strategy import FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow -# Define strategy -strategy = FedAvg( - fraction_fit=1.0, # Select all available clients - fraction_evaluate=0.0, # Disable evaluation - min_available_clients=5, -) + +# Define metric aggregation function +def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: + # Multiply accuracy of each client by number of examples used + accuracies = [num_examples * m["accuracy"] for num_examples, m in metrics] + examples = [num_examples for num_examples, _ in metrics] + + # Aggregate and return custom metric (weighted average) + return {"accuracy": sum(accuracies) / sum(examples)} + + +ndarrays = get_weights(Net()) +parameters = ndarrays_to_parameters(ndarrays) # Flower ServerApp @@ -19,29 +29,40 @@ @app.main() def main(driver: Driver, context: Context) -> None: + # Define strategy + strategy = FedAvg( + # Select all available clients + fraction_fit=1.0, + # Disable evaluation in demo + fraction_evaluate=0.0 if IS_DEMO else context.run_config["fraction-evaluate"], + min_available_clients=5, + evaluate_metrics_aggregation_fn=weighted_average, + initial_parameters=parameters, + ) + # Construct the LegacyContext - num_rounds = int(context.run_config["num_server_rounds"]) + num_rounds = int(context.run_config["num-server-rounds"]) context = LegacyContext( context=context, config=ServerConfig(num_rounds=num_rounds), strategy=strategy, ) - # Create the workflow - workflow = DefaultWorkflow( - fit_workflow=SecAggPlusWorkflowWithLogs( - num_shares=int(context.run_config["num_shares"]), - reconstruction_threshold=int( - context.run_config["reconstruction_threshold"] - ), - timeout=float(context.run_config["timeout"]), + # Create fit workflow + if IS_DEMO: + fit_workflow = SecAggPlusWorkflowWithLogs( + num_shares=context.run_config["num-shares"], + reconstruction_threshold=context.run_config["reconstruction-threshold"], + timeout=context.run_config["timeout"], ) - # # For real-world applications, use the following code instead - # fit_workflow=SecAggPlusWorkflow( - # num_shares=int(context.run_config["num_shares"]), - # reconstruction_threshold=int(context.run_config["reconstruction_threshold"]), - # ) - ) + else: + fit_workflow = SecAggPlusWorkflow( + num_shares=context.run_config["num-shares"], + reconstruction_threshold=context.run_config["reconstruction-threshold"], + ) + + # Create the workflow + workflow = DefaultWorkflow(fit_workflow=fit_workflow) # Execute workflow(driver, context) diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py new file mode 100644 index 00000000000..2a1681d0deb --- /dev/null +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -0,0 +1,134 @@ +"""From pytorchexample: A Flower / PyTorch app.""" + +import random +from collections import OrderedDict +from unittest.mock import Mock + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from flwr_datasets.partitioner import IidPartitioner +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor + +# For real-world applications, please set it to False +IS_DEMO = True + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self): + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x): + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def make_net(seed=0): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + return Net() + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) + + +fds = None # Cache FederatedDataset + + +def load_data(partition_id: int, num_partitions: int, batch_size: int): + """Load partition CIFAR10 data.""" + if IS_DEMO: + trainloader, testloader = Mock(dataset=[0]), Mock(dataset=[0]) + return trainloader, testloader + # Only initialize `FederatedDataset` once + global fds + if fds is None: + partitioner = IidPartitioner(num_partitions=num_partitions) + fds = FederatedDataset( + dataset="uoft-cs/cifar10", + partitioners={"train": partitioner}, + ) + partition = fds.load_partition(partition_id) + # Divide data on each node: 80% train, 20% test + partition_train_test = partition.train_test_split(test_size=0.2, seed=42) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader( + partition_train_test["train"], batch_size=batch_size, shuffle=True + ) + testloader = DataLoader(partition_train_test["test"], batch_size=batch_size) + return trainloader, testloader + + +def train(net, trainloader, valloader, epochs, learning_rate, device): + """Train the model on the training set.""" + if IS_DEMO: + return {} + net.to(device) # move model to GPU if available + criterion = torch.nn.CrossEntropyLoss().to(device) + optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) + net.train() + for _ in range(epochs): + for batch in trainloader: + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + criterion(net(images.to(device)), labels.to(device)).backward() + optimizer.step() + + val_loss, val_acc = test(net, valloader, device) + + results = { + "val_loss": val_loss, + "val_accuracy": val_acc, + } + return results + + +def test(net, testloader, device): + """Validate the model on the test set.""" + if IS_DEMO: + return 0.0, 0.0 + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in testloader: + images = batch["img"].to(device) + labels = batch["label"].to(device) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + loss = loss / len(testloader) + return loss, accuracy diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index 51c345dbe3d..03e1efd9804 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,6 +1,6 @@ from logging import DEBUG, INFO -import numpy as np +from secaggexample.task import get_weights, make_net import flwr.common.recordset_compat as compat from flwr.common import Context, log, parameters_to_ndarrays @@ -26,8 +26,11 @@ class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): node_ids = [] def __call__(self, driver: Driver, context: Context) -> None: + first_3_params = get_weights(make_net())[0].flatten()[:3] _quantized = quantize( - [np.ones(3) for _ in range(5)], self.clipping_range, self.quantization_range + [first_3_params for _ in range(5)], + self.clipping_range, + self.quantization_range, ) log(INFO, "") log( @@ -36,21 +39,21 @@ def __call__(self, driver: Driver, context: Context) -> None: ) log( INFO, - "In the example, each client will upload a vector [1.0, 1.0, 1.0] instead of", + "In the example, clients will skip model training and evaluation", ) - log(INFO, "model updates for demonstration purposes.") + log(INFO, "for demonstration purposes.") log( INFO, "Client 0 is configured to drop out before uploading the masked vector.", ) log(INFO, "After quantization, the raw vectors will look like:") for i in range(1, 5): - log(INFO, "\t%s from Client %s", _quantized[i], i) + log(INFO, "\t%s from Client %s...", _quantized[i], i) log( INFO, "Numbers are rounded to integers stochastically during the quantization", ) - log(INFO, ", and thus entries may not be identical.") + log(INFO, ", and thus vectors may not be identical.") log( INFO, "The above raw vectors are hidden from the driver through adding masks.", @@ -68,8 +71,8 @@ def __call__(self, driver: Driver, context: Context) -> None: ndarrays = parameters_to_ndarrays(parameters) log( INFO, - "Weighted average of vectors (dequantized): %s", - ndarrays[0], + "Weighted average of parameters (dequantized): %s...", + ndarrays[0].flatten()[:3], ) log( INFO, @@ -93,5 +96,9 @@ def collect_masked_vectors_stage( ret = super().collect_masked_vectors_stage(driver, context, state) for node_id in state.sampled_node_ids - state.active_node_ids: log(INFO, "Client %s dropped out.", self.node_ids.index(node_id)) - log(INFO, "Obtained sum of masked vectors: %s", state.aggregate_ndarrays[1]) + log( + INFO, + "Obtained sum of masked parameters: %s...", + state.aggregate_ndarrays[1].flatten()[:3], + ) return ret From fd3ed8b214232db99c9195e55c3346ab2e6fdd1d Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Tue, 6 Aug 2024 19:05:47 +0100 Subject: [PATCH 10/36] update metadata in README.md --- examples/flower-secure-aggregation/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 23038e40e4e..7e6d9cda89c 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -1,8 +1,8 @@ --- title: Simple Secure Aggregation with Flower Example -tags: [basic, secure_aggregation, privacy] -dataset: [] -framework: [numpy] +tags: [advanced, secure_aggregation, privacy] +dataset: [CIFAR-10] +framework: [torch, torchvision] --- # Secure aggregation with Flower (the SecAgg+ protocol) @@ -29,6 +29,7 @@ flower-secure-aggregation | โ”œโ”€โ”€ __init__.py | โ”œโ”€โ”€ client_app.py # defines your ClientApp | โ”œโ”€โ”€ server_app.py # defines your ServerApp + | โ”œโ”€โ”€ task.py | โ””โ”€โ”€ workflow_with_log.py โ”œโ”€โ”€ pyproject.toml # builds your FAB, includes dependencies and configs โ””โ”€โ”€ README.md @@ -74,4 +75,4 @@ flwr run --run-config 'num_server_rounds=5' To adapt the example for real-world applications, follow these steps: 1. Set `IS_DEMO` to `False` in `./secaggexample/task.py`. -2. Adjust the `num-shares` and `reconstruction-threshold` settings in `./pyproject.toml` to suit your requirements. \ No newline at end of file +2. Adjust the `num-shares` and `reconstruction-threshold` settings in `./pyproject.toml` to suit your requirements. From 8a31a9a9e293f32f355cf2cffb254278981a4fab Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:47:39 +0100 Subject: [PATCH 11/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 7e6d9cda89c..2a593245460 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -24,15 +24,15 @@ following files: ```shell flower-secure-aggregation - | - โ”œโ”€โ”€ secaggexample - | โ”œโ”€โ”€ __init__.py - | โ”œโ”€โ”€ client_app.py # defines your ClientApp - | โ”œโ”€โ”€ server_app.py # defines your ServerApp - | โ”œโ”€โ”€ task.py - | โ””โ”€โ”€ workflow_with_log.py - โ”œโ”€โ”€ pyproject.toml # builds your FAB, includes dependencies and configs - โ””โ”€โ”€ README.md +| +โ”œโ”€โ”€ secaggexample +| โ”œโ”€โ”€ __init__.py +| โ”œโ”€โ”€ client_app.py # Defines your ClientApp +| โ”œโ”€โ”€ server_app.py # Defines your ServerApp +| โ”œโ”€โ”€ task.py +| โ””โ”€โ”€ workflow_with_log.py +โ”œโ”€โ”€ pyproject.toml # Project metadata like dependencies and configs +โ””โ”€โ”€ README.md ``` ## Install dependencies From b09bb0fb1e5a0792952042ec2c10942f01d8eeea Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:47:46 +0100 Subject: [PATCH 12/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 2a593245460..bba0ab703b0 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -1,5 +1,4 @@ --- -title: Simple Secure Aggregation with Flower Example tags: [advanced, secure_aggregation, privacy] dataset: [CIFAR-10] framework: [torch, torchvision] From c90ddfb6278cce2972c330973683e22a57111231 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:47:55 +0100 Subject: [PATCH 13/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index bba0ab703b0..c916f055824 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -8,7 +8,10 @@ framework: [torch, torchvision] The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflowWithLogs`, which is a subclass of `SecAggPlusWorkflow` that includes more detailed logging specifically designed for this example. -## Project Setup +## Set up the project + +### Clone the project + Start by cloning the example project: From 79e5fdd21f27587a555d1e5130d8e36b40c20cc1 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:48:19 +0100 Subject: [PATCH 14/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index c916f055824..99566a49922 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -43,15 +43,9 @@ flower-secure-aggregation pip install . ``` -## Run the Example - -You can run your `ClientApp` and `ServerApp` in both _simulation_ and -_deployment_ mode without making changes to the code. If you are starting -with Flower, we recommend you using the _simulation_ model as it requires -fewer components to be launched manually. By default, `flwr run` will make -use of the Simluation Engine. Refer to alternative ways of running your -Flower application including Deployment, with TLS certificates, or with -Docker later in this readme. +## Run the project + +You can run your Flower project in both _simulation_ and _deployment_ mode without making changes to the code. If you are starting with Flower, we recommend you using the _simulation_ mode as it requires fewer components to be launched manually. By default, `flwr run` will make use of the Simulation Engine. ### Run with the Simulation Engine From 1629725638c9d5c166106669486934e99b356b9e Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:49:24 +0100 Subject: [PATCH 15/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 99566a49922..a9838762f78 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -49,8 +49,6 @@ You can run your Flower project in both _simulation_ and _deployment_ mode witho ### Run with the Simulation Engine -Run: - ```bash flwr run ``` From 80a97a93ed494ac7201c79aeae11efd298c1610f Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:49:34 +0100 Subject: [PATCH 16/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index a9838762f78..276df3c8329 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -40,7 +40,7 @@ flower-secure-aggregation ## Install dependencies ```bash -pip install . +pip install -e . ``` ## Run the project From ec305232b49c12c121c15119980197f630b85f62 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:49:44 +0100 Subject: [PATCH 17/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 276df3c8329..56f3520e43a 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -37,7 +37,9 @@ flower-secure-aggregation โ””โ”€โ”€ README.md ``` -## Install dependencies +### Install dependencies and project + +Install the dependencies defined in `pyproject.toml` as well as the `secaggexample` package. ```bash pip install -e . From e90d6ebb691c0a2136213d445e503df241630798 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:49:53 +0100 Subject: [PATCH 18/36] Update examples/flower-secure-aggregation/secaggexample/server_app.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/server_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 179bb6d87f9..593f754fc95 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -41,7 +41,7 @@ def main(driver: Driver, context: Context) -> None: ) # Construct the LegacyContext - num_rounds = int(context.run_config["num-server-rounds"]) + num_rounds = context.run_config["num-server-rounds"] context = LegacyContext( context=context, config=ServerConfig(num_rounds=num_rounds), From 3efad28471deb8286e3f1ea2f7402cab9f4db818 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:50:16 +0100 Subject: [PATCH 19/36] Update examples/flower-secure-aggregation/secaggexample/workflow_with_log.py Co-authored-by: Javier --- .../secaggexample/workflow_with_log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index 03e1efd9804..07ffb5efb4a 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,3 +1,5 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + from logging import DEBUG, INFO from secaggexample.task import get_weights, make_net From 061d27c180810ea11f3f878a33ff6af5bcc75350 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:50:37 +0100 Subject: [PATCH 20/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 56f3520e43a..ccac66d35b3 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -52,7 +52,7 @@ You can run your Flower project in both _simulation_ and _deployment_ mode witho ### Run with the Simulation Engine ```bash -flwr run +flwr run . ``` You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example From 2e4a23958dd4ab9f7b77e729ae8cae55cb5ea864 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:50:53 +0100 Subject: [PATCH 21/36] Update examples/flower-secure-aggregation/README.md Co-authored-by: Javier --- examples/flower-secure-aggregation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index ccac66d35b3..1ba49e063af 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -58,7 +58,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example ```bash -flwr run --run-config 'num_server_rounds=5' +flwr run . --run-config num-server-rounds=5 ``` ### Run with the Deployment Engine From abdc1440891ab7468bc406b82f00f55476583fa9 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:51:07 +0100 Subject: [PATCH 22/36] Update examples/flower-secure-aggregation/pyproject.toml Co-authored-by: Javier --- examples/flower-secure-aggregation/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index c83363d1702..bfe97ceb49d 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ packages = ["."] [tool.flwr.app] -publisher = "flowerteam" +publisher = "flwrlabs" [tool.flwr.app.components] serverapp = "secaggexample.server_app:app" From 92b710db1aafeca9ddebf987e963cc072f71b870 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:51:23 +0100 Subject: [PATCH 23/36] Update examples/flower-secure-aggregation/pyproject.toml Co-authored-by: Javier --- examples/flower-secure-aggregation/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index bfe97ceb49d..03976c31369 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "secaggexample" version = "1.0.0" description = "Secure Aggregation in Flower" -license = { text = "Apache License (2.0)" } +license = "Apache-2.0" dependencies = [ "flwr[simulation]>=1.10.0", "flwr-datasets[vision]>=0.3.0", From 3068378856deb4e7e70249e9ec5685a757d37782 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:51:38 +0100 Subject: [PATCH 24/36] Update examples/flower-secure-aggregation/secaggexample/server_app.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/server_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 593f754fc95..91e00b7544a 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -1,3 +1,5 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + from typing import List, Tuple from secaggexample.task import IS_DEMO, Net, get_weights From 292723abdaeaec3a8848be6cc86428d06005fb5c Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:51:45 +0100 Subject: [PATCH 25/36] Update examples/flower-secure-aggregation/secaggexample/client_app.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/client_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index 5072a2a9cc7..882ce240777 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -1,3 +1,5 @@ +"""secaggexample: A Flower with SecAgg+ app.""" + import time import torch From 636aa6f87170a3d7281dcbb5a2ad614d418b485a Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:51:56 +0100 Subject: [PATCH 26/36] Update examples/flower-secure-aggregation/secaggexample/task.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py index 2a1681d0deb..6b13a1187d0 100644 --- a/examples/flower-secure-aggregation/secaggexample/task.py +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -1,4 +1,4 @@ -"""From pytorchexample: A Flower / PyTorch app.""" +"""secaggexample: A Flower with SecAgg+ app.""" import random from collections import OrderedDict From 73cadec39e29c6d0b96bf86594bfa41068567218 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 11:52:06 +0100 Subject: [PATCH 27/36] Update examples/flower-secure-aggregation/secaggexample/__init__.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/secaggexample/__init__.py b/examples/flower-secure-aggregation/secaggexample/__init__.py index beb5becac69..366ceebfae8 100644 --- a/examples/flower-secure-aggregation/secaggexample/__init__.py +++ b/examples/flower-secure-aggregation/secaggexample/__init__.py @@ -1 +1 @@ -"""secaggexample.""" +"""secaggexample: A Flower with SecAgg+ app.""" From d6f200ac661461374d14500b4ce7aac78d7b5fbe Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 12:08:34 +0100 Subject: [PATCH 28/36] move init params --- .../secaggexample/server_app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 91e00b7544a..6c92a8641e8 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -2,7 +2,7 @@ from typing import List, Tuple -from secaggexample.task import IS_DEMO, Net, get_weights +from secaggexample.task import IS_DEMO, Net, get_weights, make_net from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs from flwr.common import Context, Metrics, ndarrays_to_parameters @@ -21,16 +21,16 @@ def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: return {"accuracy": sum(accuracies) / sum(examples)} -ndarrays = get_weights(Net()) -parameters = ndarrays_to_parameters(ndarrays) - - # Flower ServerApp app = ServerApp() @app.main() def main(driver: Driver, context: Context) -> None: + # Get initial parameters + ndarrays = get_weights(make_net()) + parameters = ndarrays_to_parameters(ndarrays) + # Define strategy strategy = FedAvg( # Select all available clients From d6ece3fb5825fc8731edbca72181abd380aaf5c7 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 14:12:31 +0100 Subject: [PATCH 29/36] make is_demo a run config --- examples/flower-secure-aggregation/README.md | 8 +++++--- examples/flower-secure-aggregation/pyproject.toml | 5 ++++- .../secaggexample/client_app.py | 3 +++ .../secaggexample/server_app.py | 14 +++++++++++--- .../secaggexample/task.py | 10 +++++----- .../secaggexample/workflow_with_log.py | 8 ++++---- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 1ba49e063af..5a6b8cc1ea3 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -66,9 +66,11 @@ flwr run . --run-config num-server-rounds=5 > \[!NOTE\] > An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. -## Amend the example for practical usage +## Amend the Example for Practical Usage To adapt the example for real-world applications, follow these steps: -1. Set `IS_DEMO` to `False` in `./secaggexample/task.py`. -2. Adjust the `num-shares` and `reconstruction-threshold` settings in `./pyproject.toml` to suit your requirements. +1. Open the `./pyproject.toml` file. +2. Navigate to the `[tool.flwr.app.config]` section. +3. Set `is-demo` to `false`. +4. Adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. diff --git a/examples/flower-secure-aggregation/pyproject.toml b/examples/flower-secure-aggregation/pyproject.toml index 03976c31369..d9be719653b 100644 --- a/examples/flower-secure-aggregation/pyproject.toml +++ b/examples/flower-secure-aggregation/pyproject.toml @@ -31,10 +31,13 @@ fraction-evaluate = 0.5 local-epochs = 1 learning-rate = 0.1 batch-size = 32 -# parameters for the SecAgg+ protocol +# Parameters for the SecAgg+ protocol num-shares = 3 reconstruction-threshold = 2 +max-weight = 9000 timeout = 15.0 +# Demo flag +is-demo = true [tool.flwr.federations] default = "local-simulation" diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index 882ce240777..6283f081c4c 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -3,6 +3,7 @@ import time import torch +from secaggexample import task from secaggexample.task import Net, get_weights, load_data, set_weights, test, train from flwr.client import ClientApp, NumPyClient @@ -50,6 +51,8 @@ def evaluate(self, parameters, config): def client_fn(context: Context): """Construct a Client that will be run in a ClientApp.""" + # Set `task.is_demo` + task.is_demo = context.run_config["is-demo"] # Read the node_config to fetch data partition associated to this node partition_id = context.node_config["partition-id"] diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 6c92a8641e8..ad0e7cc342d 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -2,7 +2,8 @@ from typing import List, Tuple -from secaggexample.task import IS_DEMO, Net, get_weights, make_net +from secaggexample import task +from secaggexample.task import get_weights, make_net from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs from flwr.common import Context, Metrics, ndarrays_to_parameters @@ -27,6 +28,9 @@ def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: @app.main() def main(driver: Driver, context: Context) -> None: + # Set `task.is_demo` + task.is_demo = context.run_config["is-demo"] + # Get initial parameters ndarrays = get_weights(make_net()) parameters = ndarrays_to_parameters(ndarrays) @@ -36,7 +40,9 @@ def main(driver: Driver, context: Context) -> None: # Select all available clients fraction_fit=1.0, # Disable evaluation in demo - fraction_evaluate=0.0 if IS_DEMO else context.run_config["fraction-evaluate"], + fraction_evaluate=( + 0.0 if task.is_demo else context.run_config["fraction-evaluate"] + ), min_available_clients=5, evaluate_metrics_aggregation_fn=weighted_average, initial_parameters=parameters, @@ -51,16 +57,18 @@ def main(driver: Driver, context: Context) -> None: ) # Create fit workflow - if IS_DEMO: + if task.is_demo: fit_workflow = SecAggPlusWorkflowWithLogs( num_shares=context.run_config["num-shares"], reconstruction_threshold=context.run_config["reconstruction-threshold"], + max_weight=1, timeout=context.run_config["timeout"], ) else: fit_workflow = SecAggPlusWorkflow( num_shares=context.run_config["num-shares"], reconstruction_threshold=context.run_config["reconstruction-threshold"], + max_weight=context.run_config["max-weight"], ) # Create the workflow diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py index 6b13a1187d0..b3d85feb847 100644 --- a/examples/flower-secure-aggregation/secaggexample/task.py +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -14,7 +14,7 @@ from torchvision.transforms import Compose, Normalize, ToTensor # For real-world applications, please set it to False -IS_DEMO = True +is_demo = True class Net(nn.Module): @@ -38,7 +38,7 @@ def forward(self, x): return self.fc3(x) -def make_net(seed=0): +def make_net(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) @@ -60,7 +60,7 @@ def set_weights(net, parameters): def load_data(partition_id: int, num_partitions: int, batch_size: int): """Load partition CIFAR10 data.""" - if IS_DEMO: + if is_demo: trainloader, testloader = Mock(dataset=[0]), Mock(dataset=[0]) return trainloader, testloader # Only initialize `FederatedDataset` once @@ -93,7 +93,7 @@ def apply_transforms(batch): def train(net, trainloader, valloader, epochs, learning_rate, device): """Train the model on the training set.""" - if IS_DEMO: + if is_demo: return {} net.to(device) # move model to GPU if available criterion = torch.nn.CrossEntropyLoss().to(device) @@ -118,7 +118,7 @@ def train(net, trainloader, valloader, epochs, learning_rate, device): def test(net, testloader, device): """Validate the model on the test set.""" - if IS_DEMO: + if is_demo: return 0.0, 0.0 criterion = torch.nn.CrossEntropyLoss() correct, loss = 0, 0.0 diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index 07ffb5efb4a..aabdd567f41 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -50,15 +50,15 @@ def __call__(self, driver: Driver, context: Context) -> None: ) log(INFO, "After quantization, the raw vectors will look like:") for i in range(1, 5): - log(INFO, "\t%s from Client %s...", _quantized[i], i) + log(INFO, "\t%s... from Client %s", _quantized[i], i) log( INFO, - "Numbers are rounded to integers stochastically during the quantization", + "Numbers are rounded to integers stochastically during the quantization, ", ) - log(INFO, ", and thus vectors may not be identical.") + log(INFO, "and thus vectors may not be identical.") log( INFO, - "The above raw vectors are hidden from the driver through adding masks.", + "The above raw vectors are hidden from the ServerApp through adding masks.", ) log(INFO, "") log( From 30bd275394d4ec4e1d1013fac623bdca483009f8 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 14:28:16 +0100 Subject: [PATCH 30/36] format README.md --- examples/flower-secure-aggregation/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 5a6b8cc1ea3..c33b786e19d 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -12,7 +12,6 @@ The following steps describe how to use Secure Aggregation in flower, with `Clie ### Clone the project - Start by cloning the example project: ```shell From e0d98e1fc813b33f54a814a90131bb022b15547b Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 15:24:40 +0100 Subject: [PATCH 31/36] Update examples/flower-secure-aggregation/secaggexample/task.py Co-authored-by: Javier --- examples/flower-secure-aggregation/secaggexample/task.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py index b3d85feb847..33d7714aa3d 100644 --- a/examples/flower-secure-aggregation/secaggexample/task.py +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -120,6 +120,7 @@ def test(net, testloader, device): """Validate the model on the test set.""" if is_demo: return 0.0, 0.0 + net.to(device) # move model to GPU if available criterion = torch.nn.CrossEntropyLoss() correct, loss = 0, 0.0 with torch.no_grad(): From 4ccd30bf9a139aa39d4a7ef8970b104a899804a1 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 7 Aug 2024 15:48:38 +0100 Subject: [PATCH 32/36] update based on comments --- .../secaggexample/client_app.py | 20 +++++++++++-------- .../secaggexample/server_app.py | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index 6283f081c4c..0614e867b07 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -13,13 +13,15 @@ # Define Flower Client class FlowerClient(NumPyClient): - def __init__(self, trainloader, valloader, local_epochs, learning_rate): + def __init__(self, trainloader, valloader, local_epochs, learning_rate, timeout): self.net = Net() self.trainloader = trainloader self.valloader = valloader self.local_epochs = local_epochs self.lr = learning_rate self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + # For demonstration purposes only + self.timeout = timeout def fit(self, parameters, config): """Train the model with data of this client.""" @@ -35,11 +37,12 @@ def fit(self, parameters, config): ret_vec = get_weights(self.net) # Force a significant delay for testing purposes - if "drop" in config and config["drop"]: - print(f"Client dropped for testing purposes.") - time.sleep(15) - else: - print(f"Client uploading parameters: {ret_vec[0].flatten()[:3]}...") + if task.is_demo: + if config.get("drop", False): + print(f"Client dropped for testing purposes.") + time.sleep(self.timeout) + else: + print(f"Client uploading parameters: {ret_vec[0].flatten()[:3]}...") return ret_vec, len(self.trainloader.dataset), results def evaluate(self, parameters, config): @@ -53,6 +56,7 @@ def client_fn(context: Context): """Construct a Client that will be run in a ClientApp.""" # Set `task.is_demo` task.is_demo = context.run_config["is-demo"] + timeout = context.run_config["timeout"] # Read the node_config to fetch data partition associated to this node partition_id = context.node_config["partition-id"] @@ -62,10 +66,10 @@ def client_fn(context: Context): batch_size = context.run_config["batch-size"] trainloader, valloader = load_data(partition_id, num_partitions, batch_size) local_epochs = context.run_config["local-epochs"] - learning_rate = context.run_config["learning-rate"] + lr = context.run_config["learning-rate"] # Return Client instance - return FlowerClient(trainloader, valloader, local_epochs, learning_rate).to_client() + return FlowerClient(trainloader, valloader, local_epochs, lr, timeout).to_client() # Flower ClientApp diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index ad0e7cc342d..3274c857474 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -57,6 +57,8 @@ def main(driver: Driver, context: Context) -> None: ) # Create fit workflow + # For further information, please see: + # https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html if task.is_demo: fit_workflow = SecAggPlusWorkflowWithLogs( num_shares=context.run_config["num-shares"], From 341c6813e9d318d113f357315b994c048a3456b1 Mon Sep 17 00:00:00 2001 From: jafermarq Date: Wed, 7 Aug 2024 19:06:14 +0100 Subject: [PATCH 33/36] minor updates --- examples/flower-secure-aggregation/README.md | 15 +++--- .../secaggexample/client_app.py | 48 +++++++++++-------- .../secaggexample/server_app.py | 11 ++--- .../secaggexample/task.py | 9 +--- 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index c33b786e19d..34783ed85e8 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -60,16 +60,13 @@ You can also override some of the settings for your `ClientApp` and `ServerApp` flwr run . --run-config num-server-rounds=5 ``` +To adapt the example for a practial usage, set `is-demo=false` like shown below. You might want to adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. You can override those via `--run-config` as well. + +```bash +flwr run . --run-config is-demo=false +``` + ### Run with the Deployment Engine > \[!NOTE\] > An update to this example will show how to run this Flower project with the Deployment Engine and TLS certificates, or with Docker. - -## Amend the Example for Practical Usage - -To adapt the example for real-world applications, follow these steps: - -1. Open the `./pyproject.toml` file. -2. Navigate to the `[tool.flwr.app.config]` section. -3. Set `is-demo` to `false`. -4. Adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. diff --git a/examples/flower-secure-aggregation/secaggexample/client_app.py b/examples/flower-secure-aggregation/secaggexample/client_app.py index 0614e867b07..7f4fd54b98b 100644 --- a/examples/flower-secure-aggregation/secaggexample/client_app.py +++ b/examples/flower-secure-aggregation/secaggexample/client_app.py @@ -3,17 +3,18 @@ import time import torch -from secaggexample import task -from secaggexample.task import Net, get_weights, load_data, set_weights, test, train - from flwr.client import ClientApp, NumPyClient from flwr.client.mod import secaggplus_mod from flwr.common import Context +from secaggexample.task import Net, get_weights, load_data, set_weights, test, train + # Define Flower Client class FlowerClient(NumPyClient): - def __init__(self, trainloader, valloader, local_epochs, learning_rate, timeout): + def __init__( + self, trainloader, valloader, local_epochs, learning_rate, timeout, is_demo + ): self.net = Net() self.trainloader = trainloader self.valloader = valloader @@ -22,22 +23,25 @@ def __init__(self, trainloader, valloader, local_epochs, learning_rate, timeout) self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # For demonstration purposes only self.timeout = timeout + self.is_demo = is_demo def fit(self, parameters, config): """Train the model with data of this client.""" set_weights(self.net, parameters) - results = train( - self.net, - self.trainloader, - self.valloader, - self.local_epochs, - self.lr, - self.device, - ) + results = {} + if not self.is_demo: + results = train( + self.net, + self.trainloader, + self.valloader, + self.local_epochs, + self.lr, + self.device, + ) ret_vec = get_weights(self.net) # Force a significant delay for testing purposes - if task.is_demo: + if self.is_demo: if config.get("drop", False): print(f"Client dropped for testing purposes.") time.sleep(self.timeout) @@ -48,15 +52,14 @@ def fit(self, parameters, config): def evaluate(self, parameters, config): """Evaluate the model on the data this client has.""" set_weights(self.net, parameters) - loss, accuracy = test(self.net, self.valloader, self.device) + loss, accuracy = 0.0, 0.0 + if not self.is_demo: + loss, accuracy = test(self.net, self.valloader, self.device) return loss, len(self.valloader.dataset), {"accuracy": accuracy} def client_fn(context: Context): """Construct a Client that will be run in a ClientApp.""" - # Set `task.is_demo` - task.is_demo = context.run_config["is-demo"] - timeout = context.run_config["timeout"] # Read the node_config to fetch data partition associated to this node partition_id = context.node_config["partition-id"] @@ -64,12 +67,19 @@ def client_fn(context: Context): # Read run_config to fetch hyperparameters relevant to this run batch_size = context.run_config["batch-size"] - trainloader, valloader = load_data(partition_id, num_partitions, batch_size) + is_demo = context.run_config["is-demo"] + trainloader, valloader = load_data( + partition_id, num_partitions, batch_size, is_demo + ) local_epochs = context.run_config["local-epochs"] lr = context.run_config["learning-rate"] + # For demostrations purposes only + timeout = context.run_config["timeout"] # Return Client instance - return FlowerClient(trainloader, valloader, local_epochs, lr, timeout).to_client() + return FlowerClient( + trainloader, valloader, local_epochs, lr, timeout, is_demo + ).to_client() # Flower ClientApp diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 3274c857474..1498a1c4317 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -2,7 +2,6 @@ from typing import List, Tuple -from secaggexample import task from secaggexample.task import get_weights, make_net from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs @@ -28,8 +27,8 @@ def weighted_average(metrics: List[Tuple[int, Metrics]]) -> Metrics: @app.main() def main(driver: Driver, context: Context) -> None: - # Set `task.is_demo` - task.is_demo = context.run_config["is-demo"] + + is_demo = context.run_config["is-demo"] # Get initial parameters ndarrays = get_weights(make_net()) @@ -40,9 +39,7 @@ def main(driver: Driver, context: Context) -> None: # Select all available clients fraction_fit=1.0, # Disable evaluation in demo - fraction_evaluate=( - 0.0 if task.is_demo else context.run_config["fraction-evaluate"] - ), + fraction_evaluate=(0.0 if is_demo else context.run_config["fraction-evaluate"]), min_available_clients=5, evaluate_metrics_aggregation_fn=weighted_average, initial_parameters=parameters, @@ -59,7 +56,7 @@ def main(driver: Driver, context: Context) -> None: # Create fit workflow # For further information, please see: # https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html - if task.is_demo: + if is_demo: fit_workflow = SecAggPlusWorkflowWithLogs( num_shares=context.run_config["num-shares"], reconstruction_threshold=context.run_config["reconstruction-threshold"], diff --git a/examples/flower-secure-aggregation/secaggexample/task.py b/examples/flower-secure-aggregation/secaggexample/task.py index 33d7714aa3d..e9cca8ef911 100644 --- a/examples/flower-secure-aggregation/secaggexample/task.py +++ b/examples/flower-secure-aggregation/secaggexample/task.py @@ -13,9 +13,6 @@ from torch.utils.data import DataLoader from torchvision.transforms import Compose, Normalize, ToTensor -# For real-world applications, please set it to False -is_demo = True - class Net(nn.Module): """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" @@ -58,7 +55,7 @@ def set_weights(net, parameters): fds = None # Cache FederatedDataset -def load_data(partition_id: int, num_partitions: int, batch_size: int): +def load_data(partition_id: int, num_partitions: int, batch_size: int, is_demo: bool): """Load partition CIFAR10 data.""" if is_demo: trainloader, testloader = Mock(dataset=[0]), Mock(dataset=[0]) @@ -93,8 +90,6 @@ def apply_transforms(batch): def train(net, trainloader, valloader, epochs, learning_rate, device): """Train the model on the training set.""" - if is_demo: - return {} net.to(device) # move model to GPU if available criterion = torch.nn.CrossEntropyLoss().to(device) optimizer = torch.optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9) @@ -118,8 +113,6 @@ def train(net, trainloader, valloader, epochs, learning_rate, device): def test(net, testloader, device): """Validate the model on the test set.""" - if is_demo: - return 0.0, 0.0 net.to(device) # move model to GPU if available criterion = torch.nn.CrossEntropyLoss() correct, loss = 0, 0.0 From c30cf4d69384a960b941f7b827dc04be047e7566 Mon Sep 17 00:00:00 2001 From: jafermarq Date: Wed, 7 Aug 2024 19:14:13 +0100 Subject: [PATCH 34/36] updating logging and readme --- examples/flower-secure-aggregation/README.md | 2 +- .../flower-secure-aggregation/secaggexample/server_app.py | 4 ++++ .../secaggexample/workflow_with_log.py | 5 +---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index 34783ed85e8..c8fce7836cb 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -57,7 +57,7 @@ flwr run . You can also override some of the settings for your `ClientApp` and `ServerApp` defined in `pyproject.toml`. For example ```bash -flwr run . --run-config num-server-rounds=5 +flwr run . --run-config num-server-rounds=5,learning-rate=0.25 ``` To adapt the example for a practial usage, set `is-demo=false` like shown below. You might want to adjust the `num-shares` and `reconstruction-threshold` settings to suit your requirements. You can override those via `--run-config` as well. diff --git a/examples/flower-secure-aggregation/secaggexample/server_app.py b/examples/flower-secure-aggregation/secaggexample/server_app.py index 1498a1c4317..a332ffb9eca 100644 --- a/examples/flower-secure-aggregation/secaggexample/server_app.py +++ b/examples/flower-secure-aggregation/secaggexample/server_app.py @@ -1,11 +1,14 @@ """secaggexample: A Flower with SecAgg+ app.""" +from logging import DEBUG from typing import List, Tuple from secaggexample.task import get_weights, make_net from secaggexample.workflow_with_log import SecAggPlusWorkflowWithLogs from flwr.common import Context, Metrics, ndarrays_to_parameters +from flwr.common.logger import update_console_handler + from flwr.server import Driver, LegacyContext, ServerApp, ServerConfig from flwr.server.strategy import FedAvg from flwr.server.workflow import DefaultWorkflow, SecAggPlusWorkflow @@ -57,6 +60,7 @@ def main(driver: Driver, context: Context) -> None: # For further information, please see: # https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html if is_demo: + update_console_handler(DEBUG, True, True) fit_workflow = SecAggPlusWorkflowWithLogs( num_shares=context.run_config["num-shares"], reconstruction_threshold=context.run_config["reconstruction-threshold"], diff --git a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py index aabdd567f41..b2e457484de 100644 --- a/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py +++ b/examples/flower-secure-aggregation/secaggexample/workflow_with_log.py @@ -1,12 +1,11 @@ """secaggexample: A Flower with SecAgg+ app.""" -from logging import DEBUG, INFO +from logging import INFO from secaggexample.task import get_weights, make_net import flwr.common.recordset_compat as compat from flwr.common import Context, log, parameters_to_ndarrays -from flwr.common.logger import update_console_handler from flwr.common.secure_aggregation.quantization import quantize from flwr.server import Driver, LegacyContext from flwr.server.workflow.constant import MAIN_PARAMS_RECORD @@ -15,8 +14,6 @@ WorkflowState, ) -update_console_handler(DEBUG, True, True) - class SecAggPlusWorkflowWithLogs(SecAggPlusWorkflow): """The SecAggPlusWorkflow augmented for this example. From e356f44f97defa27cc7de7d70930d50b4a075f57 Mon Sep 17 00:00:00 2001 From: jafermarq Date: Wed, 7 Aug 2024 19:48:12 +0100 Subject: [PATCH 35/36] update top part of readme --- examples/flower-secure-aggregation/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index c8fce7836cb..c793fb170a7 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -6,7 +6,7 @@ framework: [torch, torchvision] # Secure aggregation with Flower (the SecAgg+ protocol) -The following steps describe how to use Secure Aggregation in flower, with `ClientApp` using `secaggplus_mod` and `ServerApp` using `SecAggPlusWorkflowWithLogs`, which is a subclass of `SecAggPlusWorkflow` that includes more detailed logging specifically designed for this example. +The following steps describe how to use Flower's built-in Secure Aggregation components. This example demonstrates how to apply `SecAgg+` to the same federated learning workload as in the [quickstart-pytorch](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example. The `ServerApp` uses the [`SecAggPlusWorkflow`](https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html#secaggplusworkflow) while `ClientApp` uses the [`secaggplus_mod`](https://flower.ai/docs/framework/ref-api/flwr.client.mod.secaggplus_mod.html#flwr.client.mod.secaggplus_mod). To introduce the various steps involved in `SecAgg+`, this example introduces as a sub-class of `SecAggPlusWorkflow` the `SecAggPlusWorkflowWithLogs`. It is enabled by default, but you can disable (see later in this readme). ## Set up the project @@ -30,8 +30,8 @@ flower-secure-aggregation | โ”œโ”€โ”€ __init__.py | โ”œโ”€โ”€ client_app.py # Defines your ClientApp | โ”œโ”€โ”€ server_app.py # Defines your ServerApp -| โ”œโ”€โ”€ task.py -| โ””โ”€โ”€ workflow_with_log.py +| โ”œโ”€โ”€ task.py # Defines your model, training and data loading +| โ””โ”€โ”€ workflow_with_log.py # Defines a workflow used when `is-demo=true` โ”œโ”€โ”€ pyproject.toml # Project metadata like dependencies and configs โ””โ”€โ”€ README.md ``` From be9d0cb88705d71157be750639fa070e47a4251d Mon Sep 17 00:00:00 2001 From: jafermarq Date: Wed, 7 Aug 2024 20:26:30 +0100 Subject: [PATCH 36/36] format --- examples/flower-secure-aggregation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flower-secure-aggregation/README.md b/examples/flower-secure-aggregation/README.md index c793fb170a7..9e92aed01d9 100644 --- a/examples/flower-secure-aggregation/README.md +++ b/examples/flower-secure-aggregation/README.md @@ -6,7 +6,7 @@ framework: [torch, torchvision] # Secure aggregation with Flower (the SecAgg+ protocol) -The following steps describe how to use Flower's built-in Secure Aggregation components. This example demonstrates how to apply `SecAgg+` to the same federated learning workload as in the [quickstart-pytorch](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example. The `ServerApp` uses the [`SecAggPlusWorkflow`](https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html#secaggplusworkflow) while `ClientApp` uses the [`secaggplus_mod`](https://flower.ai/docs/framework/ref-api/flwr.client.mod.secaggplus_mod.html#flwr.client.mod.secaggplus_mod). To introduce the various steps involved in `SecAgg+`, this example introduces as a sub-class of `SecAggPlusWorkflow` the `SecAggPlusWorkflowWithLogs`. It is enabled by default, but you can disable (see later in this readme). +The following steps describe how to use Flower's built-in Secure Aggregation components. This example demonstrates how to apply `SecAgg+` to the same federated learning workload as in the [quickstart-pytorch](https://github.com/adap/flower/tree/main/examples/quickstart-pytorch) example. The `ServerApp` uses the [`SecAggPlusWorkflow`](https://flower.ai/docs/framework/ref-api/flwr.server.workflow.SecAggPlusWorkflow.html#secaggplusworkflow) while `ClientApp` uses the [`secaggplus_mod`](https://flower.ai/docs/framework/ref-api/flwr.client.mod.secaggplus_mod.html#flwr.client.mod.secaggplus_mod). To introduce the various steps involved in `SecAgg+`, this example introduces as a sub-class of `SecAggPlusWorkflow` the `SecAggPlusWorkflowWithLogs`. It is enabled by default, but you can disable (see later in this readme). ## Set up the project