Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/SK-707 | Flower client example #537

Merged
merged 40 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
50557cd
add flower client example
viktorvaladi Feb 28, 2024
d290402
clarify readme
viktorvaladi Feb 29, 2024
a1b6982
exclude dir floating imports
viktorvaladi Feb 29, 2024
b748904
gh wf
viktorvaladi Feb 29, 2024
5a4b1be
new flower adapter
viktorvaladi Mar 12, 2024
f83e7fd
formatting
viktorvaladi Mar 12, 2024
8e45c2a
formatting
viktorvaladi Mar 12, 2024
fcb4689
formatting...
viktorvaladi Mar 12, 2024
8f17732
Updated readme, added Dockerfile, added init file
Mar 19, 2024
936e7ae
Updated readme
Mar 20, 2024
2d148b8
point at stable
viktorvaladi Mar 20, 2024
8803bd3
Language fixes
Mar 20, 2024
acf6c04
move adapter to fedn utils
viktorvaladi Mar 21, 2024
1e978b6
add adapter in fedn utils
viktorvaladi Mar 21, 2024
e4df154
readme fix
viktorvaladi Mar 26, 2024
5954bfd
Merge branch 'master' into feature/SK-707
Apr 1, 2024
489f15a
Example updated to run natively in the venv
Apr 2, 2024
43aabb4
fix readme
Apr 2, 2024
2a29cec
python3.9 -> python3 in venv
Apr 3, 2024
5335477
Updated dependcies
Apr 3, 2024
0de5e18
add results for flower compat
viktorvaladi Apr 3, 2024
53705e7
Deps
Apr 3, 2024
61d09c0
change to fit new clientapp
viktorvaladi Apr 3, 2024
ce75cfb
Revert venv changes
Apr 3, 2024
b64bd31
Update run.sh
ahellander Apr 4, 2024
a2c7e29
Code checks
Apr 4, 2024
56e109f
Updated README, removed build.sh, instructions in readme instead
Apr 4, 2024
bc423eb
Polish readme
Apr 6, 2024
a0ee701
Fix
Apr 6, 2024
e6e6cb1
update gitignore
viktorvaladi Apr 8, 2024
d4c5721
update gitignore
viktorvaladi Apr 8, 2024
eb75a8e
updater adapter
viktorvaladi Apr 8, 2024
9c4a424
update adapter
viktorvaladi Apr 8, 2024
8ba7cbc
linting
viktorvaladi Apr 8, 2024
1854451
linting
viktorvaladi Apr 8, 2024
afe46f6
linting
viktorvaladi Apr 8, 2024
3c0006d
get_parameters and assertion added
viktorvaladi Apr 8, 2024
79c60c3
linting
viktorvaladi Apr 8, 2024
02dc0b6
linting
viktorvaladi Apr 8, 2024
30b82f2
readme typo fix
viktorvaladi Apr 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .ci/tests/examples/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ helper="$2"
>&2 echo "Start FEDn"
pushd "examples/$example"

docker-compose \
docker compose \
-f ../../docker-compose.yaml \
-f docker-compose.override.yaml \
up -d --build
Expand Down Expand Up @@ -43,7 +43,7 @@ docker-compose \
".$example/bin/python" ../../.ci/tests/examples/api_test.py get_client_config --output ../../client.yaml

# Redeploy clients with config
docker-compose \
docker compose \
-f ../../docker-compose.yaml \
-f docker-compose.override.yaml \
-f ../../.ci/tests/examples/compose-client-settings.override.yaml \
Expand All @@ -56,4 +56,4 @@ docker-compose \
".$example/bin/python" ../../.ci/tests/examples/api_test.py test_api_get_methods

popd
>&2 echo "Test completed successfully"
>&2 echo "Test completed successfully"
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
Loading