diff --git a/examples/async-simulation/.gitignore b/examples/async-simulation/.gitignore new file mode 100644 index 000000000..4ab9fa59f --- /dev/null +++ b/examples/async-simulation/.gitignore @@ -0,0 +1,6 @@ +data +*.npz +*.tgz +*.tar.gz +.async-simulation +client.yaml \ No newline at end of file diff --git a/examples/async-simulation/Experiment.ipynb b/examples/async-simulation/Experiment.ipynb new file mode 100644 index 000000000..c13ac2dbf --- /dev/null +++ b/examples/async-simulation/Experiment.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "622f7047", + "metadata": {}, + "source": [ + "## FEDn API Example\n", + "\n", + "This notebook provides an example of how to use the FEDn API to organize experiments and to analyze validation results. We will here run one training session using FedAvg and one session using FedAdam and compare the results.\n", + "\n", + "When you start this tutorial you should have a deployed FEDn Network up and running, and you should have created the compute package and the initial model, see the README for instructions." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "743dfe47", + "metadata": {}, + "outputs": [], + "source": [ + "from fedn import APIClient\n", + "from fedn.dashboard.plots import Plot\n", + "from fedn.network.clients.client import Client\n", + "import uuid\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import collections\n", + "import copy" + ] + }, + { + "cell_type": "markdown", + "id": "1046a4e5", + "metadata": {}, + "source": [ + "We make a client connection to the FEDn API service. Here we assume that FEDn is deployed locally in pseudo-distributed mode with default ports." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1061722d", + "metadata": {}, + "outputs": [], + "source": [ + "DISCOVER_HOST = '127.0.0.1'\n", + "DISCOVER_PORT = 8092\n", + "client = APIClient(DISCOVER_HOST, DISCOVER_PORT)" + ] + }, + { + "cell_type": "markdown", + "id": "07f69f5f", + "metadata": {}, + "source": [ + "Initialize FEDn with the compute package and seed model. Note that these files needs to be created separately by follwing instructions in the README." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "5107f6f9", + "metadata": {}, + "outputs": [], + "source": [ + "client.set_package('package.tgz', 'numpyhelper')\n", + "client.set_initial_model('seed.npz')\n", + "seed_model = client.get_initial_model()" + ] + }, + { + "cell_type": "markdown", + "id": "bcd55791", + "metadata": {}, + "source": [ + "Start clients" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "98692466", + "metadata": {}, + "outputs": [], + "source": [ + "config = {'discover_host': DISCOVER_HOST, 'discover_port': DISCOVER_PORT, 'token': None, 'name': 'testclient',\n", + " 'client_id': 1, 'remote_compute_context': True, 'force_ssl': False, 'dry_run': False, 'secure': False,\n", + " 'preshared_cert': False, 'verify': False, 'preferred_combiner': False,\n", + " 'validator': True, 'trainer': True, 'init': None, 'logfile': 'test.log', 'heartbeat_interval': 2,\n", + " 'reconnect_after_missed_heartbeat': 30}\n", + "\n", + "N_CLIENTS = 1\n", + "clients = []\n", + "for i in range(N_CLIENTS):\n", + " config_i = copy.deepcopy(config)\n", + " config['name'] = 'client{}'.format(i)\n", + " clients.append(Client(config))" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "63f5593b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'count': 1,\n", + " 'result': [{'combiner': 'combiner',\n", + " 'combiner_preferred': False,\n", + " 'id': 'client-0',\n", + " 'ip': '172.18.0.1',\n", + " 'last_seen': '',\n", + " 'status': 'available'}]}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.list_clients()" + ] + }, + { + "cell_type": "markdown", + "id": "4e26c50b", + "metadata": {}, + "source": [ + "Next we start a training session using FedAvg:" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "f0380d35", + "metadata": {}, + "outputs": [], + "source": [ + "session_config_fedavg = {\n", + " \"helper\": \"numpyhelper\",\n", + " \"session_id\": \"experiment_fedavg2\",\n", + " \"aggregator\": \"fedavg\",\n", + " \"model_id\": seed_model['model_id'],\n", + " \"rounds\": 3,\n", + " \"validate\": False\n", + " }\n", + "\n", + "result_fedavg = client.start_session(**session_config_fedavg)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "add70e99", + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'APIClient' object has no attribute '_detach'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[21], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m c \u001b[38;5;129;01min\u001b[39;00m clients:\n\u001b[0;32m----> 2\u001b[0m \u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_detach\u001b[49m()\n", + "\u001b[0;31mAttributeError\u001b[0m: 'APIClient' object has no attribute '_detach'" + ] + } + ], + "source": [ + "for c in clients:\n", + " client._detach()" + ] + }, + { + "cell_type": "markdown", + "id": "4eeea5aa", + "metadata": {}, + "source": [ + "Wait until the session finished. Then run a session using FedAdam:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4f70d7d9", + "metadata": {}, + "outputs": [], + "source": [ + "session_config_fedopt = {\n", + " \"helper\": \"numpyhelper\",\n", + " \"session_id\": \"experiment_fedopt2\",\n", + " \"aggregator\": \"fedopt\",\n", + " \"model_id\": seed_model['model_id'],\n", + " \"rounds\": 10\n", + " }\n", + "\n", + "result_fedopt = client.start_session(**session_config_fedopt)" + ] + }, + { + "cell_type": "markdown", + "id": "29552af9", + "metadata": {}, + "source": [ + "Next, we retrive all model validations from all clients, extract the training accuracy metric, and compute its mean value accross all clients" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "11fd17ef", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "models = client.list_models(session_id = \"experiment_fedavg\")\n", + "\n", + "validations = []\n", + "acc = collections.OrderedDict()\n", + "for model in models[\"result\"]:\n", + " model_id = model[\"model\"]\n", + " validations = client.list_validations(modelId=model_id)\n", + "\n", + " for _ , validation in validations.items(): \n", + " metrics = json.loads(validation['data'])\n", + " try:\n", + " acc[model_id].append(metrics['training_accuracy'])\n", + " except KeyError: \n", + " acc[model_id] = [metrics['training_accuracy']]\n", + " \n", + "mean_acc_fedavg = []\n", + "for model, data in acc.items():\n", + " mean_acc_fedavg.append(np.mean(data))\n", + "mean_acc_fedavg.reverse()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "900eb0a7", + "metadata": {}, + "outputs": [], + "source": [ + "models = client.list_models(session_id = \"experiment_fedopt\")\n", + "\n", + "validations = []\n", + "acc = collections.OrderedDict()\n", + "for model in models[\"result\"]:\n", + " model_id = model[\"model\"]\n", + " validations = client.list_validations(modelId=model_id)\n", + " for _ , validation in validations.items(): \n", + " metrics = json.loads(validation['data'])\n", + " try:\n", + " acc[model_id].append(metrics['training_accuracy'])\n", + " except KeyError: \n", + " acc[model_id] = [metrics['training_accuracy']]\n", + " \n", + "mean_acc_fedopt = []\n", + "for model, data in acc.items():\n", + " mean_acc_fedopt.append(np.mean(data))\n", + "mean_acc_fedopt.reverse()" + ] + }, + { + "cell_type": "markdown", + "id": "40db4542", + "metadata": {}, + "source": [ + "Finally, plot the resulting accuracy" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "d064aaf9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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F1+Ml5cXXl5efPvtt8ycORMvLy8cDofZJdYqkZGRxMbGltvWtm1bkpOTTapIHnvsMcaOHcutt95Khw4duP3223n44YeZOnWq2aXVehEREQCkpaWV256Wllb2mjspjFRjhmHwwAMPsHDhQr7++muaNm1qdkm1Wt++ffn9999Zv3592dK1a1eGDRvG+vXrsdlsZpdYq/Tq1euUru7btm2jSZMmJlUk+fn5WK3lP3ZsNhtOp9OkiuS4pk2bEhERQWJiYtm27Oxsfv75Z3r27On29nWbphobNWoU8+fPZ9GiRQQFBZXd1wsJCcHPz8/k6mqfoKCgU57XCQgIoF69enqOxwQPP/wwl156Kc888wy33HILq1ev5rXXXuO1114zu7Raa9CgQTz99NM0btyYdu3asW7dOqZPn87f/vY3s0urFXJzc9mxY0fZ17t372b9+vWEhobSuHFjRo8ezZQpU2jZsiVNmzZlwoQJREVFlfW4cStDqi3gtMsbb7xhdmlyTO/evY2HHnrI7DJqrc8++8xo3769YbfbjTZt2hivvfaa2SXVatnZ2cZDDz1kNG7c2PD19TWaNWtmjB8/3igqKjK7tFphxYoVp/3MGDFihGEYhuF0Oo0JEyYY4eHhht1uN/r27WskJSVVSW0aZ0RERERMpWdGRERExFQKIyIiImIqhRERERExlcKIiIiImEphREREREylMCIiIiKmUhgRERERUymMiIiIiKkURkRERMRUCiMiIiJiKoURERERMZXCiIiIiJjq/wHq35Vjnbzm4wAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x = range(1,len(mean_acc_fedavg)+1)\n", + "plt.plot(x, mean_acc_fedavg, x, mean_acc_fedopt)\n", + "plt.legend(['FedAvg', 'FedAdam'])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/async-simulation/README.md b/examples/async-simulation/README.md index c6d1074b8..859a3035e 100644 --- a/examples/async-simulation/README.md +++ b/examples/async-simulation/README.md @@ -11,51 +11,37 @@ This example is intended as a test for asynchronous clients. Clone FEDn and locate into this directory. ```sh git clone https://github.com/scaleoutsystems/fedn.git -cd fedn/examples/mnist-pytorch +cd fedn/examples/async-simulation ``` ### Preparing the environment, the local data, the compute package and seed model -Start by initializing a virtual enviroment with all of the required dependencies. -``` -bin/init_venv.sh -``` -Then, to get the data you can run the following script. -``` -bin/get_data -``` +Install FEDn and dependencies (we recommend using a virtual environment): -The next command splits the data in 2 parts for the clients. ``` -bin/split_data +pip install -e ../../fedn +pip install -r requirements.txt ``` -> **Note**: run with `--n_splits=N` to split in *N* parts. Create the compute package and a seed model that you will be asked to upload in the next step. ``` -bin/build.sh +tar -cvzf package.tgz ``` -> The files location will be `package/package.tgz` and `seed.npz`. -### Deploy FEDn -Now we are ready to deploy FEDn with `docker-compose`. ``` -docker-compose -f ../../docker-compose.yaml up -d minio mongo mongo-express reducer combiner +python client/entrypoint init_seed ``` -### Initialize the federated model -Now navigate to http://localhost:8090 to see the reducer UI. You will be asked to upload the compute package and the seed model that you created in the previous step. Make sure to choose the "PyTorch" helper. +### Deploy FEDn and two clients +docker-compose -f ../../docker-compose.yaml -f docker-compose.override.yaml up -### Attach clients -To attach clients to the network, use the docker-compose.override.yaml template to start 2 clients: +### Initialize the federated model +See 'Experiments.pynb' or 'launch_client.py' to set the package and seed model. -``` -docker-compose -f ../../docker-compose.yaml -f docker-compose.override.yaml up client -``` > **Note**: run with `--scale client=N` to start *N* clients. ### Run federated training -Finally, you can start the experiment from the "control" tab of the UI. +See 'Experiment.ipynb'. ## Clean up You can clean up by running `docker-compose down`. diff --git a/examples/async-simulation/init_fedn.py b/examples/async-simulation/init_fedn.py index 69e89bace..23078fcd9 100644 --- a/examples/async-simulation/init_fedn.py +++ b/examples/async-simulation/init_fedn.py @@ -1,8 +1,8 @@ from fedn import APIClient -DISCOVER_HOST = '54.208.105.152' +DISCOVER_HOST = '127.0.0.1' DISCOVER_PORT = 8092 client = APIClient(DISCOVER_HOST, DISCOVER_PORT) -client.set_package('package.tar.gz', 'numpyarrayhelper') +client.set_package('package.tgz', 'numpyhelper') client.set_initial_model('seed.npz') diff --git a/examples/async-simulation/launch_clients.py b/examples/async-simulation/launch_clients.py index 40bfb8f9d..c9cab276e 100644 --- a/examples/async-simulation/launch_clients.py +++ b/examples/async-simulation/launch_clients.py @@ -1,3 +1,17 @@ +"""This scripts starts N_CLIENTS using the SDK. + +If you are running with a local deploy of FEDn +using docker compose, you need to make sure that clients +are able to resolver the name "combiner" to 127.0.0.1 + +One way to accomplish this is to edit your /etc/host, +adding the line: + +combiner 127.0.0.1 + +""" + + import copy import time @@ -5,7 +19,8 @@ DISCOVER_HOST = '127.0.0.1' DISCOVER_PORT = 8092 -N_CLIENTS = 1 +N_CLIENTS = 5 +CLIENTS_AVAILABLE_TIME = 120 config = {'discover_host': DISCOVER_HOST, 'discover_port': DISCOVER_PORT, 'token': None, 'name': 'testclient', 'client_id': 1, 'remote_compute_context': True, 'force_ssl': False, 'dry_run': False, 'secure': False, @@ -13,12 +28,14 @@ 'validator': True, 'trainer': True, 'init': None, 'logfile': 'test.log', 'heartbeat_interval': 2, 'reconnect_after_missed_heartbeat': 30} +# Start up N_CLIENTS clients clients = [] for i in range(N_CLIENTS): config_i = copy.deepcopy(config) config['name'] = 'client{}'.format(i) clients.append(Client(config)) -time.sleep(120) +# Disconnect clients after some time +time.sleep(CLIENTS_AVAILABLE_TIME) for client in clients: client._detach() diff --git a/examples/async-simulation/seed.npz b/examples/async-simulation/seed.npz deleted file mode 100644 index f51517cc8..000000000 Binary files a/examples/async-simulation/seed.npz and /dev/null differ diff --git a/examples/mnist-keras/bin/init_venv_macm1.sh b/examples/mnist-keras/bin/init_venv_macm1.sh new file mode 100755 index 000000000..d60f602d5 --- /dev/null +++ b/examples/mnist-keras/bin/init_venv_macm1.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# Init venv +python3 -m venv .mnist-keras + +# Pip deps +.mnist-keras/bin/pip install --upgrade pip +.mnist-keras/bin/pip install -e ../../fedn +.mnist-keras/bin/pip install -r requirements-macos.txt diff --git a/examples/mnist-keras/requirements-macos.txt b/examples/mnist-keras/requirements-macos.txt new file mode 100644 index 000000000..4770f97cd --- /dev/null +++ b/examples/mnist-keras/requirements-macos.txt @@ -0,0 +1,4 @@ +tensorflow-macos +tensorflow-metal +fire==0.3.1 +docker==5.0.2