From 1daee437a6482f1e80ce0897ced12e477525d6bf Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 11 Dec 2024 10:27:37 +0000 Subject: [PATCH] refactor(examples) Update simulation series tutorial materials (#4663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Rußmeyer <73818387+Julianrussmeyer@users.noreply.github.com> --- .../.gitignore | 3 + .../Part-I/README.md | 17 -- .../Part-I/client.py | 104 ------------ .../Part-I/conf/base.yaml | 17 -- .../Part-I/dataset.py | 73 -------- .../Part-I/main.py | 127 -------------- .../Part-I/model.py | 64 ------- .../Part-I/server.py | 59 ------- .../Part-II/README.md | 42 ----- .../Part-II/client.py | 78 --------- .../Part-II/conf/base.yaml | 16 -- .../Part-II/conf/model/net.yaml | 3 - .../Part-II/conf/strategy/fedadam.yaml | 21 --- .../Part-II/conf/strategy/fedavg.yaml | 13 -- .../Part-II/conf/toy.yaml | 70 -------- .../Part-II/conf/toy_model/mobilenetv2.yaml | 4 - .../Part-II/conf/toy_model/resnet18.yaml | 4 - .../Part-II/dataset.py | 60 ------- .../Part-II/main.py | 82 --------- .../Part-II/model.py | 67 -------- .../Part-II/server.py | 39 ----- .../Part-II/toy.py | 111 ------------ .../README.md | 137 +++++++++++---- .../my-awesome-app/.gitignore | 160 ++++++++++++++++++ .../my-awesome-app/README.md | 34 ++++ .../my-awesome-app/my_awesome_app/__init__.py | 1 + .../my_awesome_app/client_app.py | 96 +++++++++++ .../my_awesome_app/my_strategy.py | 75 ++++++++ .../my_awesome_app/server_app.py | 99 +++++++++++ .../my-awesome-app/my_awesome_app/task.py | 138 +++++++++++++++ .../my-awesome-app/pyproject.toml | 42 +++++ .../requirements.txt | 2 - 32 files changed, 756 insertions(+), 1102 deletions(-) create mode 100644 examples/flower-simulation-step-by-step-pytorch/.gitignore delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/README.md delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/client.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/conf/base.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/main.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/model.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-I/server.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/README.md delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/client.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/base.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/model/net.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/strategy/fedadam.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/strategy/fedavg.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/toy.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/toy_model/mobilenetv2.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/conf/toy_model/resnet18.yaml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/dataset.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/main.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/model.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/server.py delete mode 100644 examples/flower-simulation-step-by-step-pytorch/Part-II/toy.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/.gitignore create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/README.md create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/my_awesome_app/__init__.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/my_awesome_app/client_app.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/my_awesome_app/my_strategy.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/my_awesome_app/server_app.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/my_awesome_app/task.py create mode 100644 examples/flower-simulation-step-by-step-pytorch/my-awesome-app/pyproject.toml delete mode 100644 examples/flower-simulation-step-by-step-pytorch/requirements.txt diff --git a/examples/flower-simulation-step-by-step-pytorch/.gitignore b/examples/flower-simulation-step-by-step-pytorch/.gitignore new file mode 100644 index 000000000000..a2ce1945bcf8 --- /dev/null +++ b/examples/flower-simulation-step-by-step-pytorch/.gitignore @@ -0,0 +1,3 @@ +wandb/ +global_model_round_* +*.json diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/README.md b/examples/flower-simulation-step-by-step-pytorch/Part-I/README.md deleted file mode 100644 index d961d29184de..000000000000 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# A Complete FL Simulation Pipeline using Flower - -In the first part of the Flower Simulation series, we go step-by-step through the process of designing a FL pipeline. Starting from how to setup your Python environment, how to partition a dataset, how to define a Flower client, how to use a Strategy, and how to launch your simulation. The code in this directory is the one developed in the video. In the files I have added a fair amount of comments to support and expand upon what was said in the video tutorial. - -## Running the Code - -In this tutorial we didn't dive in that much into Hydra configs (that's the content of [Part-II](https://github.com/adap/flower/tree/main/examples/flower-simulation-step-by-step-pytorch/Part-II)). However, this doesn't mean we can't easily configure our experiment directly from the command line. Let's see a couple of examples on how to run our simulation. - -```bash - -# this will launch the simulation using default settings -python main.py - -# you can override the config easily for instance -python main.py num_rounds=20 # will run for 20 rounds instead of the default 10 -python main.py config_fit.lr=0.1 # will use a larger learning rate for the clients. -``` diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py deleted file mode 100644 index 3d93510b3d0e..000000000000 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/client.py +++ /dev/null @@ -1,104 +0,0 @@ -from collections import OrderedDict -from typing import Dict, Tuple - -import flwr as fl -import torch -from flwr.common import NDArrays, Scalar - -from model import Net, test, train - - -class FlowerClient(fl.client.NumPyClient): - """Define a Flower Client.""" - - def __init__(self, trainloader, vallodaer, num_classes) -> None: - super().__init__() - - # the dataloaders that point to the data associated to this client - self.trainloader = trainloader - self.valloader = vallodaer - - # a model that is randomly initialised at first - self.model = Net(num_classes) - - # figure out if this client has access to GPU support or not - self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - def set_parameters(self, parameters): - """Receive parameters and apply them to the local model.""" - 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 get_parameters(self, config: Dict[str, Scalar]): - """Extract model parameters and return them as a list of numpy arrays.""" - - return [val.cpu().numpy() for _, val in self.model.state_dict().items()] - - def fit(self, parameters, config): - """Train model received by the server (parameters) using the data. - - that belongs to this client. Then, send it back to the server. - """ - - # copy parameters sent by the server into client's local model - self.set_parameters(parameters) - - # fetch elements in the config sent by the server. Note that having a config - # sent by the server each time a client needs to participate is a simple but - # powerful mechanism to adjust these hyperparameters during the FL process. For - # example, maybe you want clients to reduce their LR after a number of FL rounds. - # or you want clients to do more local epochs at later stages in the simulation - # you can control these by customising what you pass to `on_fit_config_fn` when - # defining your strategy. - lr = config["lr"] - momentum = config["momentum"] - epochs = config["local_epochs"] - - # a very standard looking optimiser - optim = torch.optim.SGD(self.model.parameters(), lr=lr, momentum=momentum) - - # do local training. This function is identical to what you might - # have used before in non-FL projects. For more advance FL implementation - # you might want to tweak it but overall, from a client perspective the "local - # training" can be seen as a form of "centralised training" given a pre-trained - # model (i.e. the model received from the server) - train(self.model, self.trainloader, optim, epochs, self.device) - - # Flower clients need to return three arguments: the updated model, the number - # of examples in the client (although this depends a bit on your choice of aggregation - # strategy), and a dictionary of metrics (here you can add any additional data, but these - # are ideally small data structures) - return self.get_parameters({}), len(self.trainloader), {} - - def evaluate(self, parameters: NDArrays, config: Dict[str, Scalar]): - self.set_parameters(parameters) - - loss, accuracy = test(self.model, self.valloader, self.device) - - return float(loss), len(self.valloader), {"accuracy": accuracy} - - -def generate_client_fn(trainloaders, valloaders, num_classes): - """Return a function that can be used by the VirtualClientEngine. - - to spawn a FlowerClient with client id `cid`. - """ - - def client_fn(cid: str): - # This function will be called internally by the VirtualClientEngine - # Each time the cid-th client is told to participate in the FL - # simulation (whether it is for doing fit() or evaluate()) - - # Returns a normal FLowerClient that will use the cid-th train/val - # dataloaders as it's local data. - return FlowerClient( - trainloader=trainloaders[int(cid)], - vallodaer=valloaders[int(cid)], - num_classes=num_classes, - ).to_client() - - # return the function to spawn client - return client_fn diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/conf/base.yaml b/examples/flower-simulation-step-by-step-pytorch/Part-I/conf/base.yaml deleted file mode 100644 index 24cbb8f2cf0c..000000000000 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/conf/base.yaml +++ /dev/null @@ -1,17 +0,0 @@ ---- -# this is a very minimal config file in YAML format -# it will be processed by Hydra at runtime -# you might notice it doesn't have anything special that other YAML files don't have -# check the followup tutorial on how to use Hydra in conjunction with Flower for a -# much more advanced usage of Hydra configs - -num_rounds: 10 # number of FL rounds in the experiment -num_clients: 100 # number of total clients available (this is also the number of partitions we need to create) -batch_size: 20 # batch size to use by clients during training -num_classes: 10 # number of classes in our dataset (we use MNIST) -- this tells the model how to setup its output fully-connected layer -num_clients_per_round_fit: 10 # number of clients to involve in each fit round (fit round = clients receive the model from the server and do local training) -num_clients_per_round_eval: 25 # number of clients to involve in each evaluate round (evaluate round = client only evaluate the model sent by the server on their local dataset without training it) -config_fit: # a config that each client will receive (this is send by the server) when they are sampled. This allows you to dynamically configure the training on the client side as the simulation progresses - lr: 0.01 # learning rate to use by the clients - momentum: 0.9 # momentum used by SGD optimiser on the client side - local_epochs: 1 # number of training epochs each clients does in a fit() round \ No newline at end of file diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py deleted file mode 100644 index a805906b8d42..000000000000 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/dataset.py +++ /dev/null @@ -1,73 +0,0 @@ -import torch -from torch.utils.data import DataLoader, random_split -from torchvision.datasets import MNIST -from torchvision.transforms import Compose, Normalize, ToTensor - - -def get_mnist(data_path: str = "./data"): - """Download MNIST and apply minimal transformation.""" - - tr = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))]) - - trainset = MNIST(data_path, train=True, download=True, transform=tr) - testset = MNIST(data_path, train=False, download=True, transform=tr) - - return trainset, testset - - -def prepare_dataset(num_partitions: int, batch_size: int, val_ratio: float = 0.1): - """Download MNIST and generate IID partitions.""" - - # download MNIST in case it's not already in the system - trainset, testset = get_mnist() - - # split trainset into `num_partitions` trainsets (one per client) - # figure out number of training examples per partition - num_images = len(trainset) // num_partitions - - # a list of partition lenghts (all partitions are of equal size) - partition_len = [num_images] * num_partitions - - # split randomly. This returns a list of trainsets, each with `num_images` training examples - # Note this is the simplest way of splitting this dataset. A more realistic (but more challenging) partitioning - # would induce heterogeneity in the partitions in the form of for example: each client getting a different - # amount of training examples, each client having a different distribution over the labels (maybe even some - # clients not having a single training example for certain classes). If you are curious, you can check online - # for Dirichlet (LDA) or pathological dataset partitioning in FL. A place to start is: https://arxiv.org/abs/1909.06335 - trainsets = random_split( - trainset, partition_len, torch.Generator().manual_seed(2023) - ) - - # create dataloaders with train+val support - trainloaders = [] - valloaders = [] - # for each train set, let's put aside some training examples for validation - for trainset_ in trainsets: - num_total = len(trainset_) - num_val = int(val_ratio * num_total) - num_train = num_total - num_val - - for_train, for_val = random_split( - trainset_, [num_train, num_val], torch.Generator().manual_seed(2023) - ) - - # construct data loaders and append to their respective list. - # In this way, the i-th client will get the i-th element in the trainloaders list and the i-th element in the valloaders list - trainloaders.append( - DataLoader(for_train, batch_size=batch_size, shuffle=True, num_workers=2) - ) - valloaders.append( - DataLoader(for_val, batch_size=batch_size, shuffle=False, num_workers=2) - ) - - # We leave the test set intact (i.e. we don't partition it) - # This test set will be left on the server side and we'll be used to evaluate the - # performance of the global model after each round. - # Please note that a more realistic setting would instead use a validation set on the server for - # this purpose and only use the testset after the final round. - # Also, in some settings (specially outside simulation) it might not be feasible to construct a validation - # set on the server side, therefore evaluating the global model can only be done by the clients. (see the comment - # in main.py above the strategy definition for more details on this) - testloader = DataLoader(testset, batch_size=128) - - return trainloaders, valloaders, testloader diff --git a/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py b/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py deleted file mode 100644 index 1373f24fbb11..000000000000 --- a/examples/flower-simulation-step-by-step-pytorch/Part-I/main.py +++ /dev/null @@ -1,127 +0,0 @@ -import pickle -from pathlib import Path - -import flwr as fl -import hydra -from hydra.core.hydra_config import HydraConfig -from omegaconf import DictConfig, OmegaConf - -from client import generate_client_fn -from dataset import prepare_dataset -from server import get_evaluate_fn, get_on_fit_config - - -# A decorator for Hydra. This tells hydra to by default load the config in conf/base.yaml -@hydra.main(config_path="conf", config_name="base", version_base=None) -def main(cfg: DictConfig): - ## 1. Parse config & get experiment output dir - print(OmegaConf.to_yaml(cfg)) - # Hydra automatically creates a directory for your experiments - # by default it would be in /outputs//