{ "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 the federation's CA certificate (`ca.crt`) to verify the server's certificate. Get it from the server operator after they have run `appfl-setup-ssl`. By convention the CA cert lives at `~/.appfl/ssl/ca.crt`.\n", "\n", "💡 Please check this [tutorial](https://appfl.ai/en/latest/tutorials/examples_ssl.html) for more details on generating SSL certificates for production deployments.\n", "\n", "Then point the client config at the CA cert path:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "ssl_dir = os.path.expanduser(\"~/.appfl/ssl\")\n", "client_config.comm_configs.grpc_configs.use_ssl = True\n", "client_config.comm_configs.grpc_configs.root_certificate = f\"{ssl_dir}/ca.crt\"" ] }, { "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[2026-05-31 21:49:19,715 Client1]: Logging to ./output/result_Client1_2026-05-31-21-49-19.txt\n", "/Users/zilinghan/Documents/projects/appfl/appfl-release/src/appfl/misc/utils.py:126: UserWarning: NaiveAuthenticator is a shared-secret scheme suitable only for demos and trusted-network testing. Use GlobusAuthenticator (or another identity-bound authenticator) in production.\n", " authenticator = AuthenticatorClass(**authenticator_args)\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[2026-05-31 21:49:27,337 Client1]: Round Pre Val? Time Train Loss Train Accuracy Val Loss Val Accuracy\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:28,336 Client1]: 0 Y 2.3006 15.9300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:31,240 Client1]: 0 N 2.9033 0.0662 81.5625 0.1981 94.2200\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:34,932 Client1]: 1 Y 0.1981 94.2200\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:37,824 Client1]: 1 N 2.8909 0.0178 94.8438 0.1068 96.7000\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:41,419 Client1]: 2 Y 0.1068 96.7000\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:44,365 Client1]: 2 N 2.9457 0.0129 96.4219 0.0915 97.2900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:47,988 Client1]: 3 Y 0.0915 97.2900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:50,872 Client1]: 3 N 2.8832 0.0092 97.5000 0.0579 98.2300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:54,440 Client1]: 4 Y 0.0579 98.2300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:49:57,335 Client1]: 4 N 2.8942 0.0083 97.6719 0.0527 98.4100\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:00,934 Client1]: 5 Y 0.0527 98.4100\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:03,866 Client1]: 5 N 2.9315 0.0069 98.0781 0.0468 98.4900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:07,496 Client1]: 6 Y 0.0468 98.4900\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:10,396 Client1]: 6 N 2.8989 0.0061 98.2812 0.0452 98.5600\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:13,983 Client1]: 7 Y 0.0452 98.5600\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:16,895 Client1]: 7 N 2.9114 0.0054 98.4219 0.0395 98.7300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:20,492 Client1]: 8 Y 0.0395 98.7300\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:23,389 Client1]: 8 N 2.8971 0.0048 98.7031 0.0418 98.6500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:27,019 Client1]: 9 Y 0.0418 98.6500\n", "\u001b[34m\u001b[1mappfl: ✅\u001b[0m[2026-05-31 21:50:29,948 Client1]: 9 N 2.9285 0.0051 98.4531 0.0395 98.5600\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 }