{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# FL Client over Secure RPC\n", "\n", "In this notebook, we will present how to launch a gRPC client as an FL client with an authenticator. To pair with the server notebook, we consider only one client." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "num_clients = 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load client configurations\n", "\n", "We load the configuration for the client from `examples/resources/configs/mnist/client_1.yaml`" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "client_id: Client1\n", "train_configs:\n", " device: cpu\n", " logging_output_dirname: ./output\n", " logging_output_filename: result\n", "data_configs:\n", " dataset_path: ./resources/dataset/mnist_dataset.py\n", " dataset_name: get_mnist\n", " dataset_kwargs:\n", " num_clients: 2\n", " client_id: 0\n", " partition_strategy: class_noniid\n", " visualization: true\n", " output_dirname: ./output\n", " output_filename: visualization.pdf\n", "comm_configs:\n", " grpc_configs:\n", " server_uri: localhost:50051\n", " max_message_size: 1048576\n", " use_ssl: false\n", "\n" ] } ], "source": [ "from omegaconf import OmegaConf\n", "\n", "client_config_file = \"../../examples/resources/configs/mnist/client_1.yaml\"\n", "client_config = OmegaConf.load(client_config_file)\n", "print(OmegaConf.to_yaml(client_config))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "💡 We need to change the relative path in `data_configs.dataset_path` to point to the right file relative to this notebook.\n", "\n", "💡 We also need to change `data_configs.dataset_kwargs.num_clients` to 1 to make sure we only partition the MNIST dataset to one client split. We change `data_configs.dataset_kwargs.visualization` to False as well." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "client_config.data_configs.dataset_path = (\n", " \"../../examples/resources/dataset/mnist_dataset.py\"\n", ")\n", "client_config.data_configs.dataset_kwargs.num_clients = num_clients\n", "client_config.data_configs.dataset_kwargs.visualization = False" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create secure SSL channel and authenticator\n", "\n", "The client requires a root certificate to verify the server certificate. In this example, we provide that [root certificate](https://github.com/APPFL/APPFL/blob/main/src/appfl/comm/grpc/credentials/root.crt), assuming that the server uses self-signed [certificate](https://github.com/APPFL/APPFL/blob/main/src/appfl/comm/grpc/credentials/localhost.crt) and [key](https://github.com/APPFL/APPFL/blob/main/src/appfl/comm/grpc/credentials/localhost.key) provided by gRPC official documentation.\n", "\n", "💡 Please check this [tutorial](https://appfl.ai/en/latest/tutorials/examples_ssl.html) for more details on how to generate SSL certificates for securing the gRPC connections in practice.\n", "\n", "To use the provided root certificate, user just to need to set the following. If the user would like to use his own root certificate, just change this to the file path to the local root certificate." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "client_config.comm_configs.grpc_configs.use_ssl = True\n", "client_config.comm_configs.grpc_configs.root_certificate = (\n", " \"../../src/appfl/comm/grpc/credentials/root.crt\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We also need to set configurations to use the naive authenticator and provide the `auth_token` agreed with the server for authentication." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "client_config.comm_configs.grpc_configs.use_authenticator = True\n", "client_config.comm_configs.grpc_configs.authenticator = \"NaiveAuthenticator\"\n", "client_config.comm_configs.grpc_configs.authenticator_args = {\n", " \"auth_token\": \"A_SECRET_DEMO_TOKEN\"\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create the client agent and communicator\n", "\n", "Now we are ready to create the client agent using the `client_agent` defined and modified above, as well as a `GRPCClientCommunicator` to send request to the server.\n", "\n", "⚠️ Please make sure that you have started the server from the other notebook!" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:31,188 Client1]: Logging to ./output/result_Client1_2025-01-08-09-56-31.txt\n" ] } ], "source": [ "from appfl.agent import ClientAgent\n", "from appfl.comm.grpc import GRPCClientCommunicator\n", "\n", "client_agent = ClientAgent(client_agent_config=client_config)\n", "client_communicator = GRPCClientCommunicator(\n", " client_id=client_agent.get_id(),\n", " **client_config.comm_configs.grpc_configs,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Start the FL experiment\n", "\n", "Client start the FL experiment by doing the following things:\n", "\n", "- Obtain general client-side configurations from the server and load them\n", "- Obtain the initial global model from the server\n", "- *[Optional]* Send the number of local data to the server\n", "- Iteratively train the model and update the global model until receiving a `DONE` status flag from the server.\n", "\n", "💡 The server is also logging several information regarding the recipe of client requests." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:38,152 Client1]: Round Pre Val? Time Train Loss Train Accuracy Val Loss Val Accuracy\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:39,176 Client1]: 0 Y 2.3006 15.9300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:42,203 Client1]: 0 N 3.0259 0.0621 82.6719 0.1759 94.5900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:43,288 Client1]: 1 Y 0.1759 94.5900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:46,287 Client1]: 1 N 2.9982 0.0181 94.7969 0.1036 96.7900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:47,328 Client1]: 2 Y 0.1036 96.7900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:50,102 Client1]: 2 N 2.7733 0.0125 96.4062 0.0782 97.5500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:51,153 Client1]: 3 Y 0.0782 97.5500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:54,004 Client1]: 3 N 2.8507 0.0100 97.1719 0.0651 97.9100\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:55,064 Client1]: 4 Y 0.0651 97.9100\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:58,036 Client1]: 4 N 2.9708 0.0081 97.6406 0.0603 98.0500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:56:59,089 Client1]: 5 Y 0.0603 98.0500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:01,960 Client1]: 5 N 2.8703 0.0072 97.9531 0.0503 98.4800\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:03,026 Client1]: 6 Y 0.0503 98.4800\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:05,943 Client1]: 6 N 2.9161 0.0065 98.0000 0.0406 98.7400\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:07,040 Client1]: 7 Y 0.0406 98.7400\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:09,872 Client1]: 7 N 2.8316 0.0052 98.3906 0.0523 98.3900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:10,958 Client1]: 8 Y 0.0523 98.3900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:13,716 Client1]: 8 N 2.7568 0.0065 98.0781 0.0383 98.7300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:14,771 Client1]: 9 Y 0.0383 98.7300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2025-01-08 09:57:17,594 Client1]: 9 N 2.8216 0.0049 98.6562 0.0402 98.7100\n" ] }, { "data": { "text/plain": [ "{}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Obtain general client-side configurations from the server and load them\n", "client_config = client_communicator.get_configuration()\n", "client_agent.load_config(client_config)\n", "\n", "# Obtain the initial global model from the server\n", "init_global_model = client_communicator.get_global_model(init_model=True)\n", "client_agent.load_parameters(init_global_model)\n", "\n", "# Send the number of local data to the server\n", "sample_size = client_agent.get_sample_size()\n", "client_communicator.invoke_custom_action(\n", " action=\"set_sample_size\", sample_size=sample_size\n", ")\n", "\n", "while True:\n", " client_agent.train()\n", " local_model = client_agent.get_parameters()\n", " if isinstance(local_model, tuple):\n", " local_model, meta_data_local = local_model[0], local_model[1]\n", " else:\n", " meta_data_local = {}\n", " new_global_model, metadata = client_communicator.update_global_model(\n", " local_model, **meta_data_local\n", " )\n", " if metadata[\"status\"] == \"DONE\":\n", " break\n", " client_agent.load_parameters(new_global_model)\n", "client_communicator.invoke_custom_action(action=\"close_connection\")" ] } ], "metadata": { "kernelspec": { "display_name": "appfl", "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.14" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }