Skip to content

Commit

Permalink
Feature/SK-707 | Flower client example (#537)
Browse files Browse the repository at this point in the history
* add flower client example

* clarify readme

* exclude dir floating imports

* gh wf

* new flower adapter

* formatting

* formatting

* formatting...

* Updated readme, added Dockerfile, added init file

* Updated readme

* point at stable

* Language fixes

* move adapter to fedn utils

* add adapter in fedn utils

* readme fix

* Example updated to run natively in the venv

* fix readme

* python3.9 -> python3 in venv

* Updated dependcies

* add results for flower compat

* Deps

* change to fit new clientapp

* Revert venv changes

* Update run.sh

* Code checks

* Updated README, removed build.sh, instructions in readme instead

* Polish readme

* Fix

* update gitignore

* update gitignore

* updater adapter

* update adapter

* linting

* linting

* linting

* get_parameters and assertion added

* linting

* linting

* readme typo fix

---------

Co-authored-by: Andreas Hellander <[email protected]>
Co-authored-by: Andreas Hellander <[email protected]>
  • Loading branch information
3 people authored Apr 8, 2024
1 parent cc63b15 commit 1f72449
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 11 deletions.
1 change: 1 addition & 0 deletions .github/workflows/code-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
--exclude-dir='.mnist-pytorch'
--exclude-dir='.mnist-keras'
--exclude-dir='docs'
--exclude-dir='flower-client'
--exclude='tests.py'
'^[ \t]+(import|from) ' -I .
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
},
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"esbonio.sphinx.confDir": "",
}
4 changes: 4 additions & 0 deletions examples/flower-client/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
data
seed.npz
*.tgz
*.tar.gz
6 changes: 6 additions & 0 deletions examples/flower-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
data
*.npz
*.tgz
*.tar.gz
.flower-client
client.yaml
10 changes: 10 additions & 0 deletions examples/flower-client/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM ghcr.io/scaleoutsystems/fedn/fedn:0.8.0

COPY requirements.txt /app/config/requirements.txt

# Install requirements
RUN python -m venv /venv \
&& /venv/bin/pip install --upgrade pip \
&& /venv/bin/pip install --no-cache-dir -r /app/config/requirements.txt \
# Clean up
&& rm -r /app/config/requirements.txt
108 changes: 108 additions & 0 deletions examples/flower-client/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Using 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).


Running the example
-------------------

See `https://fedn.readthedocs.io/en/stable/quickstart.html` for a general introduction to FEDn.
This example follows the same structure as the pytorch quickstart example.

Build a virtual environment (note that you might need to install the 'venv' package):

.. code-block::
bin/init_venv.sh
Activate the virtual environment:

.. code-block::
source .flower-client/bin/activate
Make the compute package (to be uploaded to FEDn):

.. code-block::
tar -czvf package.tgz client
Create the seed model (to be uploaded to FEDn):
.. code-block::
python client/entrypoint init_seed
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
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.
- Register a client and obtain the corresponding 'client.yaml'.

On your local machine / client (in the same virtual environment), start the FEDn client:

.. code-block::
CLIENT_NUMBER=0 FEDN_AUTH_SCHEME=Bearer fedn run client -in client.yaml --force-ssl --secure=True
Or, if you prefer to use Docker, build an image (this might take a long time):

.. code-block::
docker build -t flower-client .
Then start the client using Docker:

.. code-block::
docker run \
-v $PWD/client.yaml:/app/client.yaml \
-e CLIENT_NUMBER=0 \
-e FEDN_AUTH_SCHEME=Bearer \
flower-client run client -in client.yaml --secure=True --force-ssl
If you are running FEDn in pseudo-local mode:
------------------------------------------------------------------

Deploy a FEDn network on local host (see `https://fedn.readthedocs.io/en/stable/quickstart.html`).

Use the FEDn API Client to initalize FEDn with the compute package and seed model:

.. code-block::
python init_fedn.py
Create a file 'client.yaml' with the following content:

.. code-block::
network_id: fedn-network
discover_host: api-server
discover_port: 8092
name: myclient
Then start the client (using Docker)

.. code-block::
docker run \
-v $PWD/client.yaml:/app/client.yaml \
--network=fedn_default \
-e CLIENT_NUMBER=0 \
flower-client run client -in client.yaml
10 changes: 10 additions & 0 deletions examples/flower-client/bin/init_venv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
set -e

# Init venv
python3 -m venv .flower-client

# Pip deps
.flower-client/bin/pip install --upgrade pip
.flower-client/bin/pip install -e ../../fedn
.flower-client/bin/pip install -r requirements.txt
108 changes: 108 additions & 0 deletions examples/flower-client/client/entrypoint
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python
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())
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()
)

# 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())

# 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,
}
)
5 changes: 5 additions & 0 deletions examples/flower-client/client/fedn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
entry_points:
train:
command: python entrypoint train $ENTRYPOINT_OPTS
validate:
command: python entrypoint validate $ENTRYPOINT_OPTS
42 changes: 42 additions & 0 deletions examples/flower-client/client/flwr_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""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__()
print(f"STARTED CLIENT WITH CID {cid}")
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,
)
Loading

0 comments on commit 1f72449

Please sign in to comment.