From d8999299494a0faf5047dcb1db60cf337edc2c4f Mon Sep 17 00:00:00 2001 From: viktorvaladi Date: Thu, 18 Apr 2024 14:04:16 +0200 Subject: [PATCH 1/5] update flwr docs --- examples/flower-client/README.rst | 27 ++++- examples/flower-client/client/entrypoint.py | 2 +- .../client835-20240418-111128/entrypoint.py | 107 ++++++++++++++++++ .../client835-20240418-111128/fedn.yaml | 8 ++ .../client835-20240418-111128/flwr_client.py | 41 +++++++ .../client835-20240418-111128/flwr_task.py | 95 ++++++++++++++++ .../client835-20240418-111128/python_env.yaml | 11 ++ examples/flower-client/package/entrypoint.py | 107 ++++++++++++++++++ examples/flower-client/package/fedn.yaml | 8 ++ examples/flower-client/package/flwr_client.py | 41 +++++++ examples/flower-client/package/flwr_task.py | 95 ++++++++++++++++ .../flower-client/package/python_env.yaml | 11 ++ 12 files changed, 548 insertions(+), 5 deletions(-) create mode 100755 examples/flower-client/client835-20240418-111128/entrypoint.py create mode 100644 examples/flower-client/client835-20240418-111128/fedn.yaml create mode 100644 examples/flower-client/client835-20240418-111128/flwr_client.py create mode 100644 examples/flower-client/client835-20240418-111128/flwr_task.py create mode 100644 examples/flower-client/client835-20240418-111128/python_env.yaml create mode 100755 examples/flower-client/package/entrypoint.py create mode 100644 examples/flower-client/package/fedn.yaml create mode 100644 examples/flower-client/package/flwr_client.py create mode 100644 examples/flower-client/package/flwr_task.py create mode 100644 examples/flower-client/package/python_env.yaml diff --git a/examples/flower-client/README.rst b/examples/flower-client/README.rst index 2093caf3f..364c80d3a 100644 --- a/examples/flower-client/README.rst +++ b/examples/flower-client/README.rst @@ -1,4 +1,4 @@ -Using Flower ClientApps in FEDn +FEDn Project: Flower ClientApps in FEDn ------------------------------- This example demonstrates how to run a Flower 'ClientApp' on FEDn. @@ -62,9 +62,7 @@ On your local machine / client, start the FEDn client: .. code-block:: - export FEDN_AUTH_SCHEME=Bearer - export FEDN_PACKAGE_EXTRACT_DIR=package - CLIENT_NUMBER=0 fedn run client -in client.yaml --secure=True --force-ssl + fedn run client -in client.yaml --secure=True --force-ssl Or, if you prefer to use Docker (this might take a long time): @@ -110,3 +108,24 @@ Then start the client (using Docker) -e FEDN_AUTH_SCHEME=Bearer \ -e FEDN_PACKAGE_EXTRACT_DIR=package \ ghcr.io/scaleoutsystems/fedn/fedn:master run client -in client.yaml + + +Scaling to multiple clients +------------------------------------------------------------------ + +To scale the experiment with additional clients on the same host, simply execute the run +command again from another terminal. If running from another host add another 'client.yaml', +install fedn and execute the run command. In both cases inject a client number as an environment +varible which is used for distributing data (see 'flwr_task.py'). + +For Unix Operating Systems: + +.. code-block:: + + CLIENT_NUMBER=0 fedn run client -in client.yaml --secure=True --force-ssl + +Using Windows PowerShell: + +.. code-block:: + + & { $env:CLIENT_NUMBER="0"; fedn run client -in client.yaml --secure=$true --force-ssl } diff --git a/examples/flower-client/client/entrypoint.py b/examples/flower-client/client/entrypoint.py index 24f20773e..e80e7d4b5 100755 --- a/examples/flower-client/client/entrypoint.py +++ b/examples/flower-client/client/entrypoint.py @@ -30,7 +30,7 @@ def save_parameters(out_path, parameters_np): helper.save(parameters_np, out_path) -def init_seed(out_path="seed.npz"): +def init_seed(out_path="../seed.npz"): """Initialize seed model and save it to file. :param out_path: The path to save the seed model to. diff --git a/examples/flower-client/client835-20240418-111128/entrypoint.py b/examples/flower-client/client835-20240418-111128/entrypoint.py new file mode 100755 index 000000000..24f20773e --- /dev/null +++ b/examples/flower-client/client835-20240418-111128/entrypoint.py @@ -0,0 +1,107 @@ +import os + +import fire +from flwr_client import app + +from fedn.utils.flowercompat.client_app_adapter import FlwrClientAppAdapter +from fedn.utils.helpers.helpers import get_helper, save_metadata, save_metrics + +HELPER_MODULE = "numpyhelper" +helper = get_helper(HELPER_MODULE) + +flwr_adapter = FlwrClientAppAdapter(app) + + +def _get_node_id(): + """Get client number from environment variable.""" + + number = os.environ.get("CLIENT_NUMBER", "0") + return int(number) + + +def save_parameters(out_path, parameters_np): + """Save model paramters to file. + + :param model: The model to serialize. + :type model: torch.nn.Module + :param out_path: The path to save to. + :type out_path: str + """ + helper.save(parameters_np, out_path) + + +def init_seed(out_path="seed.npz"): + """Initialize seed model and save it to file. + + :param out_path: The path to save the seed model to. + :type out_path: str + """ + # This calls get_parameters in the flower client which needs to be implemented. + parameters_np = flwr_adapter.init_parameters(partition_id=_get_node_id(), config={}) + save_parameters(out_path, parameters_np) + + +def train(in_model_path, out_model_path): + """Complete a model update. + + Load model paramters from in_model_path (managed by the FEDn client), + perform a model update through the flower client, and write updated paramters + to out_model_path (picked up by the FEDn client). + + :param in_model_path: The path to the input model. + :type in_model_path: str + :param out_model_path: The path to save the output model to. + :type out_model_path: str + """ + parameters_np = helper.load(in_model_path) + + # Train on flower client + params, num_examples = flwr_adapter.train( + parameters=parameters_np, partition_id=_get_node_id(), config={} + ) + + # Metadata needed for aggregation server side + metadata = { + # num_examples are mandatory + "num_examples": num_examples, + } + + # Save JSON metadata file (mandatory) + save_metadata(metadata, out_model_path) + + # Save model update (mandatory) + save_parameters(out_model_path, params) + + +def validate(in_model_path, out_json_path, data_path=None): + """Validate model on the clients test dataset. + + :param in_model_path: The path to the input model. + :type in_model_path: str + :param out_json_path: The path to save the output JSON to. + :type out_json_path: str + :param data_path: The path to the data file. + :type data_path: str + """ + parameters_np = helper.load(in_model_path) + + loss, accuracy = flwr_adapter.evaluate(parameters_np, partition_id=_get_node_id(), config={}) + + # JSON schema + report = { + "test_loss": loss, + "test_accuracy": accuracy, + } + print(f"Loss: {loss}, accuracy: {accuracy}") + # Save JSON + save_metrics(report, out_json_path) + + +if __name__ == "__main__": + fire.Fire( + { + "init_seed": init_seed, + "train": train, + "validate": validate, + } + ) diff --git a/examples/flower-client/client835-20240418-111128/fedn.yaml b/examples/flower-client/client835-20240418-111128/fedn.yaml new file mode 100644 index 000000000..9fb33f18f --- /dev/null +++ b/examples/flower-client/client835-20240418-111128/fedn.yaml @@ -0,0 +1,8 @@ +python_env: python_env.yaml +entry_points: + build: + command: python entrypoint.py init_seed + train: + command: python entrypoint.py train + validate: + command: python entrypoint.py validate \ No newline at end of file diff --git a/examples/flower-client/client835-20240418-111128/flwr_client.py b/examples/flower-client/client835-20240418-111128/flwr_client.py new file mode 100644 index 000000000..297df3ca7 --- /dev/null +++ b/examples/flower-client/client835-20240418-111128/flwr_client.py @@ -0,0 +1,41 @@ +"""Flower client code using the ClientApp abstraction. +Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. +""" + +from flwr.client import ClientApp, NumPyClient +from flwr_task import (DEVICE, Net, get_weights, load_data, set_weights, test, + train) + + +# Define FlowerClient and client_fn +class FlowerClient(NumPyClient): + def __init__(self, cid) -> None: + super().__init__() + self.net = Net().to(DEVICE) + self.trainloader, self.testloader = load_data( + partition_id=int(cid), num_clients=10 + ) + + def get_parameters(self, config): + return [val.cpu().numpy() for _, val in self.net.state_dict().items()] + + def fit(self, parameters, config): + set_weights(self.net, parameters) + train(self.net, self.trainloader, epochs=3) + return get_weights(self.net), len(self.trainloader.dataset), {} + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.testloader) + return loss, len(self.testloader.dataset), {"accuracy": accuracy} + + +def client_fn(cid: str): + """Create and return an instance of Flower `Client`.""" + return FlowerClient(cid).to_client() + + +# Flower ClientApp +app = ClientApp( + client_fn=client_fn, +) diff --git a/examples/flower-client/client835-20240418-111128/flwr_task.py b/examples/flower-client/client835-20240418-111128/flwr_task.py new file mode 100644 index 000000000..dea53d9cc --- /dev/null +++ b/examples/flower-client/client835-20240418-111128/flwr_task.py @@ -0,0 +1,95 @@ +"""Flower client code for helper functions. +Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. +""" + +from collections import OrderedDict + +import torch +import torch.nn as nn +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor +from tqdm import tqdm + +DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self) -> None: + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def load_data(partition_id, num_clients): + """Load partition CIFAR10 data.""" + fds = FederatedDataset(dataset="cifar10", partitioners={"train": num_clients}) + 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) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + return trainloader, testloader + + +def train(net, trainloader, epochs): + """Train the model on the training set.""" + criterion = torch.nn.CrossEntropyLoss() + optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) + for _ in range(epochs): + for batch in tqdm(trainloader, "Training"): + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() + optimizer.step() + + +def test(net, testloader): + """Validate the model on the test set.""" + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in tqdm(testloader, "Testing"): + images = batch["img"].to(DEVICE) + labels = batch["label"].to(DEVICE) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + return loss, accuracy + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) diff --git a/examples/flower-client/client835-20240418-111128/python_env.yaml b/examples/flower-client/client835-20240418-111128/python_env.yaml new file mode 100644 index 000000000..b06804313 --- /dev/null +++ b/examples/flower-client/client835-20240418-111128/python_env.yaml @@ -0,0 +1,11 @@ +name: flower-client +build_dependencies: + - pip + - setuptools + - wheel==0.37.1 +dependencies: + - torch==2.2.1 + - torchvision==0.17.1 + - fire==0.3.1 + - fedn[flower]==0.9.0b2 + - flwr-datasets[vision]==0.1.0 \ No newline at end of file diff --git a/examples/flower-client/package/entrypoint.py b/examples/flower-client/package/entrypoint.py new file mode 100755 index 000000000..24f20773e --- /dev/null +++ b/examples/flower-client/package/entrypoint.py @@ -0,0 +1,107 @@ +import os + +import fire +from flwr_client import app + +from fedn.utils.flowercompat.client_app_adapter import FlwrClientAppAdapter +from fedn.utils.helpers.helpers import get_helper, save_metadata, save_metrics + +HELPER_MODULE = "numpyhelper" +helper = get_helper(HELPER_MODULE) + +flwr_adapter = FlwrClientAppAdapter(app) + + +def _get_node_id(): + """Get client number from environment variable.""" + + number = os.environ.get("CLIENT_NUMBER", "0") + return int(number) + + +def save_parameters(out_path, parameters_np): + """Save model paramters to file. + + :param model: The model to serialize. + :type model: torch.nn.Module + :param out_path: The path to save to. + :type out_path: str + """ + helper.save(parameters_np, out_path) + + +def init_seed(out_path="seed.npz"): + """Initialize seed model and save it to file. + + :param out_path: The path to save the seed model to. + :type out_path: str + """ + # This calls get_parameters in the flower client which needs to be implemented. + parameters_np = flwr_adapter.init_parameters(partition_id=_get_node_id(), config={}) + save_parameters(out_path, parameters_np) + + +def train(in_model_path, out_model_path): + """Complete a model update. + + Load model paramters from in_model_path (managed by the FEDn client), + perform a model update through the flower client, and write updated paramters + to out_model_path (picked up by the FEDn client). + + :param in_model_path: The path to the input model. + :type in_model_path: str + :param out_model_path: The path to save the output model to. + :type out_model_path: str + """ + parameters_np = helper.load(in_model_path) + + # Train on flower client + params, num_examples = flwr_adapter.train( + parameters=parameters_np, partition_id=_get_node_id(), config={} + ) + + # Metadata needed for aggregation server side + metadata = { + # num_examples are mandatory + "num_examples": num_examples, + } + + # Save JSON metadata file (mandatory) + save_metadata(metadata, out_model_path) + + # Save model update (mandatory) + save_parameters(out_model_path, params) + + +def validate(in_model_path, out_json_path, data_path=None): + """Validate model on the clients test dataset. + + :param in_model_path: The path to the input model. + :type in_model_path: str + :param out_json_path: The path to save the output JSON to. + :type out_json_path: str + :param data_path: The path to the data file. + :type data_path: str + """ + parameters_np = helper.load(in_model_path) + + loss, accuracy = flwr_adapter.evaluate(parameters_np, partition_id=_get_node_id(), config={}) + + # JSON schema + report = { + "test_loss": loss, + "test_accuracy": accuracy, + } + print(f"Loss: {loss}, accuracy: {accuracy}") + # Save JSON + save_metrics(report, out_json_path) + + +if __name__ == "__main__": + fire.Fire( + { + "init_seed": init_seed, + "train": train, + "validate": validate, + } + ) diff --git a/examples/flower-client/package/fedn.yaml b/examples/flower-client/package/fedn.yaml new file mode 100644 index 000000000..9fb33f18f --- /dev/null +++ b/examples/flower-client/package/fedn.yaml @@ -0,0 +1,8 @@ +python_env: python_env.yaml +entry_points: + build: + command: python entrypoint.py init_seed + train: + command: python entrypoint.py train + validate: + command: python entrypoint.py validate \ No newline at end of file diff --git a/examples/flower-client/package/flwr_client.py b/examples/flower-client/package/flwr_client.py new file mode 100644 index 000000000..297df3ca7 --- /dev/null +++ b/examples/flower-client/package/flwr_client.py @@ -0,0 +1,41 @@ +"""Flower client code using the ClientApp abstraction. +Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. +""" + +from flwr.client import ClientApp, NumPyClient +from flwr_task import (DEVICE, Net, get_weights, load_data, set_weights, test, + train) + + +# Define FlowerClient and client_fn +class FlowerClient(NumPyClient): + def __init__(self, cid) -> None: + super().__init__() + self.net = Net().to(DEVICE) + self.trainloader, self.testloader = load_data( + partition_id=int(cid), num_clients=10 + ) + + def get_parameters(self, config): + return [val.cpu().numpy() for _, val in self.net.state_dict().items()] + + def fit(self, parameters, config): + set_weights(self.net, parameters) + train(self.net, self.trainloader, epochs=3) + return get_weights(self.net), len(self.trainloader.dataset), {} + + def evaluate(self, parameters, config): + set_weights(self.net, parameters) + loss, accuracy = test(self.net, self.testloader) + return loss, len(self.testloader.dataset), {"accuracy": accuracy} + + +def client_fn(cid: str): + """Create and return an instance of Flower `Client`.""" + return FlowerClient(cid).to_client() + + +# Flower ClientApp +app = ClientApp( + client_fn=client_fn, +) diff --git a/examples/flower-client/package/flwr_task.py b/examples/flower-client/package/flwr_task.py new file mode 100644 index 000000000..dea53d9cc --- /dev/null +++ b/examples/flower-client/package/flwr_task.py @@ -0,0 +1,95 @@ +"""Flower client code for helper functions. +Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. +""" + +from collections import OrderedDict + +import torch +import torch.nn as nn +import torch.nn.functional as F +from flwr_datasets import FederatedDataset +from torch.utils.data import DataLoader +from torchvision.transforms import Compose, Normalize, ToTensor +from tqdm import tqdm + +DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + +class Net(nn.Module): + """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" + + def __init__(self) -> None: + super(Net, self).__init__() + self.conv1 = nn.Conv2d(3, 6, 5) + self.pool = nn.MaxPool2d(2, 2) + self.conv2 = nn.Conv2d(6, 16, 5) + self.fc1 = nn.Linear(16 * 5 * 5, 120) + self.fc2 = nn.Linear(120, 84) + self.fc3 = nn.Linear(84, 10) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.pool(F.relu(self.conv1(x))) + x = self.pool(F.relu(self.conv2(x))) + x = x.view(-1, 16 * 5 * 5) + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + + +def load_data(partition_id, num_clients): + """Load partition CIFAR10 data.""" + fds = FederatedDataset(dataset="cifar10", partitioners={"train": num_clients}) + 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) + pytorch_transforms = Compose( + [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] + ) + + def apply_transforms(batch): + """Apply transforms to the partition from FederatedDataset.""" + batch["img"] = [pytorch_transforms(img) for img in batch["img"]] + return batch + + partition_train_test = partition_train_test.with_transform(apply_transforms) + trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) + testloader = DataLoader(partition_train_test["test"], batch_size=32) + return trainloader, testloader + + +def train(net, trainloader, epochs): + """Train the model on the training set.""" + criterion = torch.nn.CrossEntropyLoss() + optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) + for _ in range(epochs): + for batch in tqdm(trainloader, "Training"): + images = batch["img"] + labels = batch["label"] + optimizer.zero_grad() + criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() + optimizer.step() + + +def test(net, testloader): + """Validate the model on the test set.""" + criterion = torch.nn.CrossEntropyLoss() + correct, loss = 0, 0.0 + with torch.no_grad(): + for batch in tqdm(testloader, "Testing"): + images = batch["img"].to(DEVICE) + labels = batch["label"].to(DEVICE) + outputs = net(images) + loss += criterion(outputs, labels).item() + correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() + accuracy = correct / len(testloader.dataset) + return loss, accuracy + + +def get_weights(net): + return [val.cpu().numpy() for _, val in net.state_dict().items()] + + +def set_weights(net, parameters): + params_dict = zip(net.state_dict().keys(), parameters) + state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) + net.load_state_dict(state_dict, strict=True) diff --git a/examples/flower-client/package/python_env.yaml b/examples/flower-client/package/python_env.yaml new file mode 100644 index 000000000..b06804313 --- /dev/null +++ b/examples/flower-client/package/python_env.yaml @@ -0,0 +1,11 @@ +name: flower-client +build_dependencies: + - pip + - setuptools + - wheel==0.37.1 +dependencies: + - torch==2.2.1 + - torchvision==0.17.1 + - fire==0.3.1 + - fedn[flower]==0.9.0b2 + - flwr-datasets[vision]==0.1.0 \ No newline at end of file From 60ff02a951cbbedf51ebb4090a871db58a854180 Mon Sep 17 00:00:00 2001 From: viktorvaladi Date: Thu, 18 Apr 2024 14:06:35 +0200 Subject: [PATCH 2/5] update docs --- examples/flower-client/README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/flower-client/README.rst b/examples/flower-client/README.rst index 364c80d3a..3231285a0 100644 --- a/examples/flower-client/README.rst +++ b/examples/flower-client/README.rst @@ -113,9 +113,9 @@ Then start the client (using Docker) Scaling to multiple clients ------------------------------------------------------------------ -To scale the experiment with additional clients on the same host, simply execute the run -command again from another terminal. If running from another host add another 'client.yaml', -install fedn and execute the run command. In both cases inject a client number as an environment +To scale the experiment with additional clients on the same host, execute the run command +again from another terminal. If running from another host, add another 'client.yaml', install +fedn, and execute the run command. In both cases inject a client number as an environment varible which is used for distributing data (see 'flwr_task.py'). For Unix Operating Systems: From 62884227b305dd24c269e17894042dd02c858351 Mon Sep 17 00:00:00 2001 From: viktorvaladi Date: Thu, 18 Apr 2024 14:08:46 +0200 Subject: [PATCH 3/5] cleanup generated files --- .../client835-20240418-111128/entrypoint.py | 107 ------------------ .../client835-20240418-111128/fedn.yaml | 8 -- .../client835-20240418-111128/flwr_client.py | 41 ------- .../client835-20240418-111128/flwr_task.py | 95 ---------------- .../client835-20240418-111128/python_env.yaml | 11 -- 5 files changed, 262 deletions(-) delete mode 100755 examples/flower-client/client835-20240418-111128/entrypoint.py delete mode 100644 examples/flower-client/client835-20240418-111128/fedn.yaml delete mode 100644 examples/flower-client/client835-20240418-111128/flwr_client.py delete mode 100644 examples/flower-client/client835-20240418-111128/flwr_task.py delete mode 100644 examples/flower-client/client835-20240418-111128/python_env.yaml diff --git a/examples/flower-client/client835-20240418-111128/entrypoint.py b/examples/flower-client/client835-20240418-111128/entrypoint.py deleted file mode 100755 index 24f20773e..000000000 --- a/examples/flower-client/client835-20240418-111128/entrypoint.py +++ /dev/null @@ -1,107 +0,0 @@ -import os - -import fire -from flwr_client import app - -from fedn.utils.flowercompat.client_app_adapter import FlwrClientAppAdapter -from fedn.utils.helpers.helpers import get_helper, save_metadata, save_metrics - -HELPER_MODULE = "numpyhelper" -helper = get_helper(HELPER_MODULE) - -flwr_adapter = FlwrClientAppAdapter(app) - - -def _get_node_id(): - """Get client number from environment variable.""" - - number = os.environ.get("CLIENT_NUMBER", "0") - return int(number) - - -def save_parameters(out_path, parameters_np): - """Save model paramters to file. - - :param model: The model to serialize. - :type model: torch.nn.Module - :param out_path: The path to save to. - :type out_path: str - """ - helper.save(parameters_np, out_path) - - -def init_seed(out_path="seed.npz"): - """Initialize seed model and save it to file. - - :param out_path: The path to save the seed model to. - :type out_path: str - """ - # This calls get_parameters in the flower client which needs to be implemented. - parameters_np = flwr_adapter.init_parameters(partition_id=_get_node_id(), config={}) - save_parameters(out_path, parameters_np) - - -def train(in_model_path, out_model_path): - """Complete a model update. - - Load model paramters from in_model_path (managed by the FEDn client), - perform a model update through the flower client, and write updated paramters - to out_model_path (picked up by the FEDn client). - - :param in_model_path: The path to the input model. - :type in_model_path: str - :param out_model_path: The path to save the output model to. - :type out_model_path: str - """ - parameters_np = helper.load(in_model_path) - - # Train on flower client - params, num_examples = flwr_adapter.train( - parameters=parameters_np, partition_id=_get_node_id(), config={} - ) - - # Metadata needed for aggregation server side - metadata = { - # num_examples are mandatory - "num_examples": num_examples, - } - - # Save JSON metadata file (mandatory) - save_metadata(metadata, out_model_path) - - # Save model update (mandatory) - save_parameters(out_model_path, params) - - -def validate(in_model_path, out_json_path, data_path=None): - """Validate model on the clients test dataset. - - :param in_model_path: The path to the input model. - :type in_model_path: str - :param out_json_path: The path to save the output JSON to. - :type out_json_path: str - :param data_path: The path to the data file. - :type data_path: str - """ - parameters_np = helper.load(in_model_path) - - loss, accuracy = flwr_adapter.evaluate(parameters_np, partition_id=_get_node_id(), config={}) - - # JSON schema - report = { - "test_loss": loss, - "test_accuracy": accuracy, - } - print(f"Loss: {loss}, accuracy: {accuracy}") - # Save JSON - save_metrics(report, out_json_path) - - -if __name__ == "__main__": - fire.Fire( - { - "init_seed": init_seed, - "train": train, - "validate": validate, - } - ) diff --git a/examples/flower-client/client835-20240418-111128/fedn.yaml b/examples/flower-client/client835-20240418-111128/fedn.yaml deleted file mode 100644 index 9fb33f18f..000000000 --- a/examples/flower-client/client835-20240418-111128/fedn.yaml +++ /dev/null @@ -1,8 +0,0 @@ -python_env: python_env.yaml -entry_points: - build: - command: python entrypoint.py init_seed - train: - command: python entrypoint.py train - validate: - command: python entrypoint.py validate \ No newline at end of file diff --git a/examples/flower-client/client835-20240418-111128/flwr_client.py b/examples/flower-client/client835-20240418-111128/flwr_client.py deleted file mode 100644 index 297df3ca7..000000000 --- a/examples/flower-client/client835-20240418-111128/flwr_client.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Flower client code using the ClientApp abstraction. -Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. -""" - -from flwr.client import ClientApp, NumPyClient -from flwr_task import (DEVICE, Net, get_weights, load_data, set_weights, test, - train) - - -# Define FlowerClient and client_fn -class FlowerClient(NumPyClient): - def __init__(self, cid) -> None: - super().__init__() - self.net = Net().to(DEVICE) - self.trainloader, self.testloader = load_data( - partition_id=int(cid), num_clients=10 - ) - - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in self.net.state_dict().items()] - - def fit(self, parameters, config): - set_weights(self.net, parameters) - train(self.net, self.trainloader, epochs=3) - return get_weights(self.net), len(self.trainloader.dataset), {} - - def evaluate(self, parameters, config): - set_weights(self.net, parameters) - loss, accuracy = test(self.net, self.testloader) - return loss, len(self.testloader.dataset), {"accuracy": accuracy} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - return FlowerClient(cid).to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, -) diff --git a/examples/flower-client/client835-20240418-111128/flwr_task.py b/examples/flower-client/client835-20240418-111128/flwr_task.py deleted file mode 100644 index dea53d9cc..000000000 --- a/examples/flower-client/client835-20240418-111128/flwr_task.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Flower client code for helper functions. -Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. -""" - -from collections import OrderedDict - -import torch -import torch.nn as nn -import torch.nn.functional as F -from flwr_datasets import FederatedDataset -from torch.utils.data import DataLoader -from torchvision.transforms import Compose, Normalize, ToTensor -from tqdm import tqdm - -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -class Net(nn.Module): - """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" - - def __init__(self) -> None: - super(Net, self).__init__() - self.conv1 = nn.Conv2d(3, 6, 5) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, 5) - self.fc1 = nn.Linear(16 * 5 * 5, 120) - self.fc2 = nn.Linear(120, 84) - self.fc3 = nn.Linear(84, 10) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = x.view(-1, 16 * 5 * 5) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - return self.fc3(x) - - -def load_data(partition_id, num_clients): - """Load partition CIFAR10 data.""" - fds = FederatedDataset(dataset="cifar10", partitioners={"train": num_clients}) - 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) - pytorch_transforms = Compose( - [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - def apply_transforms(batch): - """Apply transforms to the partition from FederatedDataset.""" - batch["img"] = [pytorch_transforms(img) for img in batch["img"]] - return batch - - partition_train_test = partition_train_test.with_transform(apply_transforms) - trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) - testloader = DataLoader(partition_train_test["test"], batch_size=32) - return trainloader, testloader - - -def train(net, trainloader, epochs): - """Train the model on the training set.""" - criterion = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) - for _ in range(epochs): - for batch in tqdm(trainloader, "Training"): - images = batch["img"] - labels = batch["label"] - optimizer.zero_grad() - criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() - optimizer.step() - - -def test(net, testloader): - """Validate the model on the test set.""" - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - with torch.no_grad(): - for batch in tqdm(testloader, "Testing"): - images = batch["img"].to(DEVICE) - labels = batch["label"].to(DEVICE) - outputs = net(images) - loss += criterion(outputs, labels).item() - correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() - accuracy = correct / len(testloader.dataset) - return loss, accuracy - - -def get_weights(net): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - -def set_weights(net, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) diff --git a/examples/flower-client/client835-20240418-111128/python_env.yaml b/examples/flower-client/client835-20240418-111128/python_env.yaml deleted file mode 100644 index b06804313..000000000 --- a/examples/flower-client/client835-20240418-111128/python_env.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: flower-client -build_dependencies: - - pip - - setuptools - - wheel==0.37.1 -dependencies: - - torch==2.2.1 - - torchvision==0.17.1 - - fire==0.3.1 - - fedn[flower]==0.9.0b2 - - flwr-datasets[vision]==0.1.0 \ No newline at end of file From 78c254e198cfd2c65c673f6bd09b7ffccaa2580c Mon Sep 17 00:00:00 2001 From: viktorvaladi Date: Thu, 18 Apr 2024 14:09:29 +0200 Subject: [PATCH 4/5] cleanup generated files --- examples/flower-client/package/entrypoint.py | 107 ------------------ examples/flower-client/package/fedn.yaml | 8 -- examples/flower-client/package/flwr_client.py | 41 ------- examples/flower-client/package/flwr_task.py | 95 ---------------- .../flower-client/package/python_env.yaml | 11 -- 5 files changed, 262 deletions(-) delete mode 100755 examples/flower-client/package/entrypoint.py delete mode 100644 examples/flower-client/package/fedn.yaml delete mode 100644 examples/flower-client/package/flwr_client.py delete mode 100644 examples/flower-client/package/flwr_task.py delete mode 100644 examples/flower-client/package/python_env.yaml diff --git a/examples/flower-client/package/entrypoint.py b/examples/flower-client/package/entrypoint.py deleted file mode 100755 index 24f20773e..000000000 --- a/examples/flower-client/package/entrypoint.py +++ /dev/null @@ -1,107 +0,0 @@ -import os - -import fire -from flwr_client import app - -from fedn.utils.flowercompat.client_app_adapter import FlwrClientAppAdapter -from fedn.utils.helpers.helpers import get_helper, save_metadata, save_metrics - -HELPER_MODULE = "numpyhelper" -helper = get_helper(HELPER_MODULE) - -flwr_adapter = FlwrClientAppAdapter(app) - - -def _get_node_id(): - """Get client number from environment variable.""" - - number = os.environ.get("CLIENT_NUMBER", "0") - return int(number) - - -def save_parameters(out_path, parameters_np): - """Save model paramters to file. - - :param model: The model to serialize. - :type model: torch.nn.Module - :param out_path: The path to save to. - :type out_path: str - """ - helper.save(parameters_np, out_path) - - -def init_seed(out_path="seed.npz"): - """Initialize seed model and save it to file. - - :param out_path: The path to save the seed model to. - :type out_path: str - """ - # This calls get_parameters in the flower client which needs to be implemented. - parameters_np = flwr_adapter.init_parameters(partition_id=_get_node_id(), config={}) - save_parameters(out_path, parameters_np) - - -def train(in_model_path, out_model_path): - """Complete a model update. - - Load model paramters from in_model_path (managed by the FEDn client), - perform a model update through the flower client, and write updated paramters - to out_model_path (picked up by the FEDn client). - - :param in_model_path: The path to the input model. - :type in_model_path: str - :param out_model_path: The path to save the output model to. - :type out_model_path: str - """ - parameters_np = helper.load(in_model_path) - - # Train on flower client - params, num_examples = flwr_adapter.train( - parameters=parameters_np, partition_id=_get_node_id(), config={} - ) - - # Metadata needed for aggregation server side - metadata = { - # num_examples are mandatory - "num_examples": num_examples, - } - - # Save JSON metadata file (mandatory) - save_metadata(metadata, out_model_path) - - # Save model update (mandatory) - save_parameters(out_model_path, params) - - -def validate(in_model_path, out_json_path, data_path=None): - """Validate model on the clients test dataset. - - :param in_model_path: The path to the input model. - :type in_model_path: str - :param out_json_path: The path to save the output JSON to. - :type out_json_path: str - :param data_path: The path to the data file. - :type data_path: str - """ - parameters_np = helper.load(in_model_path) - - loss, accuracy = flwr_adapter.evaluate(parameters_np, partition_id=_get_node_id(), config={}) - - # JSON schema - report = { - "test_loss": loss, - "test_accuracy": accuracy, - } - print(f"Loss: {loss}, accuracy: {accuracy}") - # Save JSON - save_metrics(report, out_json_path) - - -if __name__ == "__main__": - fire.Fire( - { - "init_seed": init_seed, - "train": train, - "validate": validate, - } - ) diff --git a/examples/flower-client/package/fedn.yaml b/examples/flower-client/package/fedn.yaml deleted file mode 100644 index 9fb33f18f..000000000 --- a/examples/flower-client/package/fedn.yaml +++ /dev/null @@ -1,8 +0,0 @@ -python_env: python_env.yaml -entry_points: - build: - command: python entrypoint.py init_seed - train: - command: python entrypoint.py train - validate: - command: python entrypoint.py validate \ No newline at end of file diff --git a/examples/flower-client/package/flwr_client.py b/examples/flower-client/package/flwr_client.py deleted file mode 100644 index 297df3ca7..000000000 --- a/examples/flower-client/package/flwr_client.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Flower client code using the ClientApp abstraction. -Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. -""" - -from flwr.client import ClientApp, NumPyClient -from flwr_task import (DEVICE, Net, get_weights, load_data, set_weights, test, - train) - - -# Define FlowerClient and client_fn -class FlowerClient(NumPyClient): - def __init__(self, cid) -> None: - super().__init__() - self.net = Net().to(DEVICE) - self.trainloader, self.testloader = load_data( - partition_id=int(cid), num_clients=10 - ) - - def get_parameters(self, config): - return [val.cpu().numpy() for _, val in self.net.state_dict().items()] - - def fit(self, parameters, config): - set_weights(self.net, parameters) - train(self.net, self.trainloader, epochs=3) - return get_weights(self.net), len(self.trainloader.dataset), {} - - def evaluate(self, parameters, config): - set_weights(self.net, parameters) - loss, accuracy = test(self.net, self.testloader) - return loss, len(self.testloader.dataset), {"accuracy": accuracy} - - -def client_fn(cid: str): - """Create and return an instance of Flower `Client`.""" - return FlowerClient(cid).to_client() - - -# Flower ClientApp -app = ClientApp( - client_fn=client_fn, -) diff --git a/examples/flower-client/package/flwr_task.py b/examples/flower-client/package/flwr_task.py deleted file mode 100644 index dea53d9cc..000000000 --- a/examples/flower-client/package/flwr_task.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Flower client code for helper functions. -Code adapted from https://github.com/adap/flower/tree/main/examples/app-pytorch. -""" - -from collections import OrderedDict - -import torch -import torch.nn as nn -import torch.nn.functional as F -from flwr_datasets import FederatedDataset -from torch.utils.data import DataLoader -from torchvision.transforms import Compose, Normalize, ToTensor -from tqdm import tqdm - -DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - - -class Net(nn.Module): - """Model (simple CNN adapted from 'PyTorch: A 60 Minute Blitz')""" - - def __init__(self) -> None: - super(Net, self).__init__() - self.conv1 = nn.Conv2d(3, 6, 5) - self.pool = nn.MaxPool2d(2, 2) - self.conv2 = nn.Conv2d(6, 16, 5) - self.fc1 = nn.Linear(16 * 5 * 5, 120) - self.fc2 = nn.Linear(120, 84) - self.fc3 = nn.Linear(84, 10) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = self.pool(F.relu(self.conv1(x))) - x = self.pool(F.relu(self.conv2(x))) - x = x.view(-1, 16 * 5 * 5) - x = F.relu(self.fc1(x)) - x = F.relu(self.fc2(x)) - return self.fc3(x) - - -def load_data(partition_id, num_clients): - """Load partition CIFAR10 data.""" - fds = FederatedDataset(dataset="cifar10", partitioners={"train": num_clients}) - 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) - pytorch_transforms = Compose( - [ToTensor(), Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - def apply_transforms(batch): - """Apply transforms to the partition from FederatedDataset.""" - batch["img"] = [pytorch_transforms(img) for img in batch["img"]] - return batch - - partition_train_test = partition_train_test.with_transform(apply_transforms) - trainloader = DataLoader(partition_train_test["train"], batch_size=32, shuffle=True) - testloader = DataLoader(partition_train_test["test"], batch_size=32) - return trainloader, testloader - - -def train(net, trainloader, epochs): - """Train the model on the training set.""" - criterion = torch.nn.CrossEntropyLoss() - optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9) - for _ in range(epochs): - for batch in tqdm(trainloader, "Training"): - images = batch["img"] - labels = batch["label"] - optimizer.zero_grad() - criterion(net(images.to(DEVICE)), labels.to(DEVICE)).backward() - optimizer.step() - - -def test(net, testloader): - """Validate the model on the test set.""" - criterion = torch.nn.CrossEntropyLoss() - correct, loss = 0, 0.0 - with torch.no_grad(): - for batch in tqdm(testloader, "Testing"): - images = batch["img"].to(DEVICE) - labels = batch["label"].to(DEVICE) - outputs = net(images) - loss += criterion(outputs, labels).item() - correct += (torch.max(outputs.data, 1)[1] == labels).sum().item() - accuracy = correct / len(testloader.dataset) - return loss, accuracy - - -def get_weights(net): - return [val.cpu().numpy() for _, val in net.state_dict().items()] - - -def set_weights(net, parameters): - params_dict = zip(net.state_dict().keys(), parameters) - state_dict = OrderedDict({k: torch.tensor(v) for k, v in params_dict}) - net.load_state_dict(state_dict, strict=True) diff --git a/examples/flower-client/package/python_env.yaml b/examples/flower-client/package/python_env.yaml deleted file mode 100644 index b06804313..000000000 --- a/examples/flower-client/package/python_env.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: flower-client -build_dependencies: - - pip - - setuptools - - wheel==0.37.1 -dependencies: - - torch==2.2.1 - - torchvision==0.17.1 - - fire==0.3.1 - - fedn[flower]==0.9.0b2 - - flwr-datasets[vision]==0.1.0 \ No newline at end of file From 932e8c1b37eeeb6d03cfefb12f7578bb0fb7be4c Mon Sep 17 00:00:00 2001 From: Fredrik Wrede Date: Thu, 18 Apr 2024 12:53:06 +0000 Subject: [PATCH 5/5] fixes --- examples/flower-client/README.rst | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/flower-client/README.rst b/examples/flower-client/README.rst index 3231285a0..dfe0bca8f 100644 --- a/examples/flower-client/README.rst +++ b/examples/flower-client/README.rst @@ -1,12 +1,12 @@ FEDn Project: Flower ClientApps in FEDn -------------------------------- +--------------------------------------- This example demonstrates how to run a Flower 'ClientApp' on FEDn. The FEDn compute package 'client/entrypoint' uses a built-in Flower compatibiltiy adapter for convenient wrapping of the Flower client. See `flwr_client.py` and `flwr_task.py` for the Flower client code (which is adapted from -https://github.com/adap/flower/tree/main/examples/app-pytorch). +https://github.com/adap/flower/tree/main/examples/app-pytorch). Running the example @@ -42,19 +42,19 @@ Next, generate a seed model (the first model in the global model trail): fedn run build --path client -Next, you will upload the compute package and seed model to +This creates a seed.npz in the root of the project folder. Next, you will upload the compute package and seed model to a FEDn network. Here you have two main options: using FEDn Studio -(recommended for new users), or a pseudo-local deployment +(recommended for new users), or a self-managed pseudo-distributed deployment on your own machine. If you are using FEDn Studio (recommended): ------------------------------------------------------ +------------------------------------------- Follow instructions here to register for Studio and start a project: https://fedn.readthedocs.io/en/stable/studio.html. In your Studio project: -- From the "Sessions" menu, upload the compute package and seed model. +- From the "Sessions" menu, upload the compute package (package.tgz) and seed model (seed.npz). - Register a client and obtain the corresponding 'client.yaml'. On your local machine / client, start the FEDn client: @@ -72,15 +72,14 @@ Or, if you prefer to use Docker (this might take a long time): docker run \ -v $PWD/client.yaml:/app/client.yaml \ -e CLIENT_NUMBER=0 \ - -e FEDN_AUTH_SCHEME=Bearer \ -e FEDN_PACKAGE_EXTRACT_DIR=package \ - ghcr.io/scaleoutsystems/fedn/fedn:master run client -in client.yaml --secure=True --force-ssl + ghcr.io/scaleoutsystems/fedn/fedn:0.9.0 run client -in client.yaml --secure=True --force-ssl -If you are running FEDn in pseudo-local mode: ------------------------------------------------------------------- +If you are running FEDn in local development mode: +-------------------------------------------------- -Deploy a FEDn network on local host (see `https://fedn.readthedocs.io/en/stable/quickstart.html`). +Deploy a FEDn network on local host (see `https://fedn.readthedocs.io/en/stable/quickstart.html#local-development-deployment-using-docker-compose`). Use the FEDn API Client to initalize FEDn with the compute package and seed model: @@ -105,9 +104,8 @@ Then start the client (using Docker) -v $PWD/client.yaml:/app/client.yaml \ --network=fedn_default \ -e CLIENT_NUMBER=0 \ - -e FEDN_AUTH_SCHEME=Bearer \ -e FEDN_PACKAGE_EXTRACT_DIR=package \ - ghcr.io/scaleoutsystems/fedn/fedn:master run client -in client.yaml + ghcr.io/scaleoutsystems/fedn/fedn:0.9.0 run client -in client.yaml Scaling to multiple clients