Tuning Federated Learning Systems

machine-learning
federated-learning
flower
Client selection, client configuration, and aggregation strategies, plus letting the server control local training through a per-round config dictionary.
Published

Aug 11, 2026

Compared to traditional centralized training, federated learning introduces additional concepts and components in the training process that can be customized and tuned. This page walks through the main ones, and then shows how a server can control client side training round by round, using a custom training schedule as the running example.

What a Federated System Lets You Tune

Client Selection

One important factor is how you select the clients that participate in each round. With five clients available, you could send the global model to just three of them in the first round, let those three participate as usual, and then select three other clients in the next round.

In a setting with only a few clients, the answer is often to just select all of them in every round. With five hospitals, you would probably use all five each round. But in settings with large numbers of clients, you typically would not. It has been shown that selecting more and more clients has diminishing returns, so in a mobile setting with millions of clients, a single round often uses just a few hundred clients, or at most a few thousand, depending on the task.

There are different strategies for picking those clients, and a very common one is to select them randomly. There are also approaches that are, strictly speaking, not federated learning but can be very useful, such as cyclic training, where the model goes to one client, gets trained, comes back to the server, and then goes to the next client, one client after another.

Client Configuration

Once clients are selected, you need to decide how to configure them. What should the client do? How long should it train? What hyperparameters should it use? Is there anything else the client needs to know to run training or evaluation the right way? The server answers these questions by sending configuration values along with the model.

Aggregation

The third commonly tuned component is aggregation. Federated averaging was introduced earlier, and there are many other approaches, such as q-FedAvg and FedAdam, that provide certain improvements over vanilla federated averaging. The Flower framework has many of them built in as strategies.

This is the system diagram again, with the two directions of a round drawn separately. Broadcasting is the server sending the current global model down to the selected clients. Uploading is those clients sending their updated parameters back. The aggregation algorithm sits between the two, and it is the only thing that changes when you swap one strategy for another. Everything else in the picture stays exactly as it is.

Global Model Server FedAvg QFedAvg FedAdam . . . Clients Uploading Broadcasting
Figure 1: Broadcast and upload paths around the server-side aggregation step in a federated learning round.

Review Questions

1. You run a federation with millions of mobile clients. Roughly how many clients should participate in a single round, and why not more?

Typically a few hundred, or at most a few thousand depending on the task. Selecting more and more clients per round has diminishing returns, so the extra communication and coordination of involving millions of clients in every round buys almost nothing over a well chosen sample.


1. What is cyclic training, and why is it strictly speaking not federated learning?

The model visits one client at a time. It is sent to a client, trained there, returned to the server, and then sent to the next client in sequence. There is no parallel local training and no aggregation of multiple simultaneous updates, which is what defines a federated round, but it can still be a useful pattern.


1. Name the three server side components of a federated system that are most commonly customized or tuned.

Client selection (which clients participate in a round), client configuration (what the selected clients are told to do, such as how long to train and with which hyperparameters), and aggregation (which algorithm merges the client updates, such as federated averaging, q-FedAvg, or FedAdam).

Lab: Server-Controlled Client Configuration

The rest of this page implements one of these ideas, a training schedule where the server changes the number of local epochs as rounds progress.

NoteLab Files Download

Everything this lab needs, next to your notebook.

  • utils3.py (4 KB), the helper file with the model, training loop, weight exchange functions, and logging setup
  • requirements.txt (1 KB), the package versions the course shipped

The MNIST dataset for this lab is fetched automatically by Flower Datasets from Hugging Face on first use, so there is nothing to download by hand.

ImportantThree changes from the course notebook

The course pinned Flower 1.10 and an older Hugging Face datasets release. This page runs on Flower 1.33 with current releases. Three things changed (updated 2026-08-31).

  • The dataset id is namespaced. The Hub now requires ylecun/mnist rather than the bare mnist the course used.
  • The example counts returned to Flower were corrected. The course returns len(self.trainloader) and len(self.testloader), which count batches rather than examples. Flower uses that number to weight each client when averaging, so it must be len(loader.dataset).
  • The evaluation loss is now a per-example mean rather than a sum of per-batch mean losses, which previously grew with the number of batches.

The server-controlled configuration, the per-round epoch schedule, the model and the five-client setup are the course’s own.

The helper file carries the same SimpleModel plus a version of train_model that takes the number of epochs as an argument, the set_weights and get_weights pair from the previous lab, a normalize transform for batches from Flower Datasets, and the notebook logging setup.

%config InlineBackend.figure_formats = ['svg']

# Page-only setting: show every worker's log lines
# (Ray deduplicates identical lines across workers by default).
import os
os.environ["RAY_DEDUP_LOGS"] = "0"

"""
Utility functions and classes for Jupyter Notebooks lessons.
"""

from collections import OrderedDict
import logging
from logging import INFO
from typing import List, Tuple, Dict, Optional, Union
import warnings

from flwr.common import (
    Metrics,
    NDArrays,
    Scalar,
    Parameters,
    FitIns,
    FitRes,
    ndarrays_to_parameters,
    Context
)
from flwr.common.logger import (
    ConsoleHandler,
    console_handler,
    FLOWER_LOGGER,
    LOG_COLORS,
    log,
)
from logging import LogRecord
from flwr.server import ClientManager, ServerAppComponents
from flwr.server.client_proxy import ClientProxy, EvaluateRes
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets
from torch.utils.data import DataLoader
from torchvision.transforms import Compose, Normalize, ToTensor


# Customize logging for the course.
class InfoFilter(logging.Filter):
    def filter(self, record):
        return record.levelno == INFO


FLOWER_LOGGER.removeHandler(console_handler)
warnings.filterwarnings("ignore")

# To filter logging coming from the Simulation Engine
# so it is more readable in notebooks
from logging import ERROR
backend_setup = {"init_args": {"logging_level": ERROR, "log_to_driver": True}}

class ConsoleHandlerV2(ConsoleHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def format(self, record: LogRecord) -> str:
        """Format function that adds colors to log level."""
        if self.json:
            log_fmt = "{lvl='%(levelname)s', time='%(asctime)s', msg='%(message)s'}"
        else:
            log_fmt = (
                f"{LOG_COLORS[record.levelname] if self.colored else ''}"
                f"%(levelname)s {'%(asctime)s' if self.timestamps else ''}"
                f"{LOG_COLORS['RESET'] if self.colored else ''}"
                f": %(message)s"
            )
        formatter = logging.Formatter(log_fmt)
        return formatter.format(record)


console_handlerv2 = ConsoleHandlerV2(
    timestamps=False,
    json=False,
    colored=True,
)
console_handlerv2.setLevel(INFO)
console_handlerv2.addFilter(InfoFilter())
FLOWER_LOGGER.addHandler(console_handlerv2)

# Route Flower logs to stdout so they render on this page.
import sys
console_handlerv2.setStream(sys.stdout)


DEVICE = torch.device("cpu")
transforms = Compose([ToTensor(), Normalize((0.5,), (0.5,))])


def normalize(batch):
    batch["image"] = [transforms(img) for img in batch["image"]]
    return batch


class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.fc = nn.Linear(784, 128)
        self.relu = nn.ReLU()
        self.out = nn.Linear(128, 10)

    def forward(self, x):
        x = torch.flatten(x, 1)
        x = self.fc(x)
        x = self.relu(x)
        x = self.out(x)
        return x


def train_model(net, trainloader, epochs: int = 1):
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(net.parameters())
    net.train()

    for _ in range(epochs):
        for batch in trainloader:
            images = batch["image"].to(DEVICE)
            labels = batch["label"].to(DEVICE)
            optimizer.zero_grad()
            loss = criterion(net(images), labels)
            loss.backward()
            optimizer.step()


def evaluate_model(net, testloader):
    net.to(DEVICE)
    criterion = torch.nn.CrossEntropyLoss()
    correct, loss = 0, 0.0
    with torch.no_grad():
        for batch in testloader:
            images = batch["image"].to(DEVICE)
            labels = batch["label"].to(DEVICE)
            outputs = net(images.to(DEVICE))
            labels = labels.to(DEVICE)
            # criterion returns the MEAN loss over the batch, so multiply by
            # the batch size to accumulate a total and divide by the dataset
            # size at the end. Summing the batch means would give a number that
            # grows with the number of batches instead of a per-example loss.
            loss += criterion(outputs, labels).item() * labels.size(0)
            correct += (
                (torch.max(outputs.data, 1)[1] == labels).sum().item()
            )
    accuracy = correct / len(testloader.dataset)
    loss = loss / len(testloader.dataset)
    return float(loss), float(accuracy)


def set_weights(net, parameters):
    params_dict = zip(net.state_dict().keys(), parameters)
    state_dict = OrderedDict(
        {k: torch.tensor(v) for k, v in params_dict}
    )
    net.load_state_dict(state_dict, strict=True)


def get_weights(net):
    ndarrays = [
        val.cpu().numpy() for _, val in net.state_dict().items()
    ]
    return ndarrays

Partitioning with Flower Datasets

The Flower imports are the same as in the previous lab, with one addition, the FederatedDataset class from the Flower Datasets library.

from flwr.client import Client, ClientApp, NumPyClient
from flwr.server import ServerApp, ServerConfig
from flwr.server.strategy import FedAvg
from flwr.simulation import run_simulation
from flwr_datasets import FederatedDataset

The earlier experiments partitioned MNIST by hand, splitting it three ways and removing digits with a helper. That was worth doing once, because it made visible exactly what each client held. It is also bookkeeping you would rather not rewrite for every new dataset, which is what FederatedDataset removes. It partitions many existing datasets the same way, so this code keeps its shape when the data changes.

Here it splits MNIST into five partitions, and the load_data function loads one partition by ID, splits it into training and testing subsets with an 80 to 20 ratio using train_test_split, applies the normalizing transform, and wraps both subsets in PyTorch data loaders. Note that each client ends up with a test set of its own and not just a training set, which is what allows a client to evaluate a model on data that only it holds.

def load_data(partition_id):
    fds = FederatedDataset(dataset="ylecun/mnist", partitioners={"train": 5})
    partition = fds.load_partition(partition_id)

    traintest = partition.train_test_split(test_size=0.2, seed=42)
    traintest = traintest.with_transform(normalize)
    trainset, testset = traintest["train"], traintest["test"]

    trainloader = DataLoader(trainset, batch_size=64, shuffle=True)
    testloader = DataLoader(testset, batch_size=64)
    return trainloader, testloader

Configuration Dictionary

Along with model parameters, the server often wants to send configuration values to clients. This is the client configuration question from earlier in concrete form. Suppose the server should control the number of local epochs each client performs, meaning the number of times a client iterates over its local dataset during training.

Someone has to decide that number, and putting the decision on the server rather than fixing it inside each client buys two things. One place controls the setting for the whole federation, and that place can change its mind between rounds without anything being reinstalled on the clients. In a real federation the clients are hospital servers or personal phones, so shipping new client code is far harder than sending a different value with the model.

To do that, you define a function fit_config that takes one argument, the current server round, and returns a configuration dictionary. The dict carries a key local_epochs with an integer value telling the client how many local epochs to train. The value varies with the round, two epochs in the first two rounds and five from round three on. Treat that particular schedule as a demonstration of the mechanism rather than a tuned recommendation. What it is built to show is that the instructions can change part way through training.

def fit_config(server_round: int):
    config_dict = {
        "local_epochs": 2 if server_round < 3 else 5,
    }
    return config_dict

To make the strategy use it, pass fit_config to federated averaging at initialization through the on_fit_config_fn parameter. The strategy then calls the function every single round, and the returned dict is included in the message sent to each client. Calling it fresh each round is what makes per-round schedules possible. The strategy also sets fraction_evaluate to zero, since no client side evaluation is needed. All five clients take part in every round because fraction_fit defaults to 1.0 and five are available. min_fit_clients=5 is a floor rather than the thing doing the selecting. Flower samples the larger of fraction_fit × available_clients and min_fit_clients, so the floor only matters when the fraction would ask for fewer.

net = SimpleModel()
params = ndarrays_to_parameters(get_weights(net))

def server_fn(context: Context):
    strategy = FedAvg(
        min_fit_clients=5,
        fraction_evaluate=0.0,
        initial_parameters=params,
        on_fit_config_fn=fit_config,  # <- NEW
    )
    config=ServerConfig(num_rounds=3)
    return ServerAppComponents(
        strategy=strategy,
        config=config,
    )
server = ServerApp(server_fn=server_fn)

Client Side

The FlowerClient class is the same as before, with one difference. Its fit method takes two main arguments, the model parameters from the server and the config dict. As usual it loads the parameters with set_weights, then extracts local_epochs from the config dict, logs it, and passes it as an additional argument to train_model so local training runs for exactly that many epochs.

class FlowerClient(NumPyClient):
    def __init__(self, net, trainloader, testloader):
        self.net = net
        self.trainloader = trainloader
        self.testloader = testloader

    def fit(self, parameters, config):
        set_weights(self.net, parameters)

        epochs = config["local_epochs"]
        log(INFO, f"client trains for {epochs} epochs")
        train_model(self.net, self.trainloader, epochs)

        # Flower weights each client's update by this number when it averages,
        # so it must be the number of EXAMPLES the client trained on. len() on a
        # DataLoader returns the number of batches, which would weight a client
        # by how its data happens to be batched rather than how much it has.
        return get_weights(self.net), len(self.trainloader.dataset), {}

    def evaluate(self, parameters, config):
        set_weights(self.net, parameters)
        loss, accuracy = evaluate_model(self.net, self.testloader)
        # Same rule here, the count is examples rather than batches.
        return loss, len(self.testloader.dataset), {"accuracy": accuracy}

The client function reads the partition ID from the context and loads that client’s data, and the ClientApp is created from it.

def client_fn(context: Context) -> Client:
    net = SimpleModel()
    partition_id = int(context.node_config["partition-id"])
    trainloader, testloader = load_data(partition_id=partition_id)
    return FlowerClient(net, trainloader, testloader).to_client()


client = ClientApp(client_fn)

What the Run Shows

Now run the server app and client app for three rounds with five simulated clients.

run_simulation(server_app=server,
               client_app=client,
               num_supernodes=5,
               backend_config=backend_setup
               )
INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout

INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout

INFO : Starting Flower ServerApp, config: num_rounds=3, no round_timeout

INFO : 

INFO : 

INFO : 

INFO : [INIT]

INFO : [INIT]

INFO : [INIT]

INFO : Using initial global parameters provided by strategy

INFO : Using initial global parameters provided by strategy

INFO : Using initial global parameters provided by strategy

INFO : Starting evaluation of initial global parameters

INFO : Starting evaluation of initial global parameters

INFO : Starting evaluation of initial global parameters

INFO : Evaluation returned no results (`None`)

INFO : Evaluation returned no results (`None`)

INFO : Evaluation returned no results (`None`)

INFO : 

INFO : 

INFO : 

INFO : [ROUND 1]

INFO : [ROUND 1]

INFO : [ROUND 1]

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)
(ClientAppActor pid=60625) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

(ClientAppActor pid=60632) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

(ClientAppActor pid=60628) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

(ClientAppActor pid=60631) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

(ClientAppActor pid=60633) Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.

(ClientAppActor pid=60632) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60632)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60632) INFO :      client trains for 2 epochs

(ClientAppActor pid=60628) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60628)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60628) INFO :      client trains for 2 epochs

(ClientAppActor pid=60625) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60625)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60625) INFO :      client trains for 2 epochs

(ClientAppActor pid=60631) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60631)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60631) INFO :      client trains for 2 epochs

(ClientAppActor pid=60633) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60633)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60633) INFO :      client trains for 2 epochs
INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : 

INFO : 

INFO : 

INFO : [ROUND 2]

INFO : [ROUND 2]

INFO : [ROUND 2]

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)
(ClientAppActor pid=60632) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60632)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60632) INFO :      client trains for 2 epochs

(ClientAppActor pid=60628) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60628)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60628) INFO :      client trains for 2 epochs

(ClientAppActor pid=60625) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60625)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60625) INFO :      client trains for 2 epochs

(ClientAppActor pid=60631) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60631)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60631) INFO :      client trains for 2 epochs

(ClientAppActor pid=60633) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60633)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60633) INFO :      client trains for 2 epochs
INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : 

INFO : 

INFO : 

INFO : [ROUND 3]

INFO : [ROUND 3]

INFO : [ROUND 3]

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)

INFO : configure_fit: strategy sampled 5 clients (out of 5)
(ClientAppActor pid=60632) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60632)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60632) INFO :      client trains for 5 epochs

(ClientAppActor pid=60625) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60625)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60625) INFO :      client trains for 5 epochs

(ClientAppActor pid=60631) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60631)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60631) INFO :      client trains for 5 epochs

(ClientAppActor pid=60633) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60633)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60633) INFO :      client trains for 5 epochs

(ClientAppActor pid=60628) /Users/hossein/Documents/website/.venv/lib/python3.13/site-packages/datasets/utils/_dill.py:385: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.

(ClientAppActor pid=60628)   obj.co_lnotab,  # for < python 3.10 [not counted in args]

(ClientAppActor pid=60628) INFO :      client trains for 5 epochs
INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : aggregate_fit: received 5 results and 0 failures

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : configure_evaluate: no clients selected, skipping evaluation

INFO : 

INFO : 

INFO : 

INFO : [SUMMARY]

INFO : [SUMMARY]

INFO : [SUMMARY]

INFO : Run finished 3 round(s) in 16.79s

INFO : Run finished 3 round(s) in 16.79s

INFO : Run finished 3 round(s) in 16.79s

INFO : 

INFO : 

INFO : 

The schedule is visible in the logs. In round one, the strategy samples five clients out of five available, and sends each the config dict along with the model parameters. Every client logs that it trains for two epochs. Round two looks the same, all five clients training for two epochs. In round three, all five clients suddenly train for five epochs instead. Nothing changed on the clients. The number of local epochs is controlled by the server, and the clients simply react to whatever configuration value they receive each round. The log also shows “no clients selected, skipping evaluation”, which is the visible effect of setting fraction_evaluate to zero.

The configuration dictionary is a flexible concept, and a perfect thing to experiment with. Many kinds of keys and values can go in it. It can carry different hyperparameter schedules, set the learning rate each client should use, and control many other aspects of the client side training process.

To summarize, federated learning introduces additional hyperparameters and concepts that control the training process. On the server side you can customize client selection, client configuration, and result aggregation. On the client side you can configure pre-processing, the local training, and any post-processing applied to the weights before they are sent back to the server. A Flower strategy is the place where the server side pieces, including server side evaluation, come together.

Review Questions

1. The server wants clients to train two local epochs in early rounds and five later. Walk through how this reaches the client.

A fit_config function maps the current server round to a config dict, for example local_epochs set to 2 for rounds one and two, and 5 afterwards. The function is passed to the strategy via on_fit_config_fn, so the strategy calls it every round and ships the dict with the model parameters. The client’s fit method reads local_epochs from the config and trains for that many epochs.


1. Why does the strategy call on_fit_config_fn every round instead of once at startup?

So the server can send different configuration values in different rounds. A schedule like two epochs early and five epochs later only works if the config is recomputed per round with the current round number as input.


1. Besides the number of local epochs, what else could the configuration dictionary control?

Any value the client side training wants to read, for example the learning rate each client should use, other hyperparameter schedules, or flags that adjust how the client runs training or evaluation. It is a general channel for per-round, server-driven configuration.

Back to top