Configure clients¶
Along with model parameters, Flower can send configuration values to clients. Configuration values can be used for various purposes. They are, for example, a popular way to control client-side hyperparameters from the server.
Configuration values¶
Configuration values are represented as a dictionary with str
keys and values of
type bool
, bytes
, double
(64-bit precision float), int
, or str
(or
equivalent types in different languages). Here is an example of a configuration
dictionary in Python:
config_dict = {
"dropout": True, # str key, bool value
"learning_rate": 0.01, # str key, float value
"batch_size": 32, # str key, int value
"optimizer": "sgd", # str key, str value
}
Flower serializes these configuration dictionaries (or config dict for short) to their ProtoBuf representation, transports them to the client using gRPC, and then deserializes them back to Python dictionaries.
Note
Currently, there is no support for directly sending collection types (e.g., Set
,
List
, Map
) as values in configuration dictionaries. There are several
workarounds to send collections as values by converting them to one of the supported
value types (and converting them back on the client-side).
One can, for example, convert a list of floating-point numbers to a JSON string, then send the JSON string using the configuration dictionary, and then convert the JSON string back to a list of floating-point numbers on the client.
Configuration through built-in strategies¶
The easiest way to send configuration values to clients is to use a built-in strategy
like FedAvg
. Built-in strategies support so-called configuration functions. A
configuration function is a function that the built-in strategy calls to get the
configuration dictionary for the current round. It then forwards the configuration
dictionary to all the clients selected during that round.
Let’s start with a simple example. Imagine we want to send (a) the batch size that the client should use, (b) the current global round of federated learning, and (c) the number of epochs to train on the client-side. Our configuration function could look like this:
def fit_config(server_round: int):
"""Return training configuration dict for each round."""
config = {
"batch_size": 32,
"current_round": server_round,
"local_epochs": 2,
}
return config
To make the built-in strategies use this function, we can pass it to FedAvg
during
initialization using the parameter on_fit_config_fn
:
strategy = FedAvg(
..., # Other FedAvg parameters
on_fit_config_fn=fit_config, # The fit_config function we defined earlier
)
One the client side, we receive the configuration dictionary in fit
:
class FlowerClient(flwr.client.NumPyClient):
def fit(parameters, config):
print(config["batch_size"]) # Prints `32`
print(config["current_round"]) # Prints `1`/`2`/`...`
print(config["local_epochs"]) # Prints `2`
# ... (rest of `fit` method)
There is also an on_evaluate_config_fn to configure evaluation, which works the same way. They are separate functions because one might want to send different configuration values to evaluate (for example, to use a different batch size).
The built-in strategies call this function every round (that is, every time Strategy.configure_fit or Strategy.configure_evaluate runs). Calling on_evaluate_config_fn every round allows us to vary/change the config dict over consecutive rounds. If we wanted to implement a hyperparameter schedule, for example, to increase the number of local epochs during later rounds, we could do the following:
def fit_config(server_round: int):
"""Return training configuration dict for each round."""
config = {
"batch_size": 32,
"current_round": server_round,
"local_epochs": 1 if server_round < 2 else 2,
}
return config
The FedAvg
strategy will call this function every round.
Configuring individual clients¶
In some cases, it is necessary to send different configuration values to different clients.
This can be achieved by customizing an existing strategy or by implementing a
custom strategy from scratch. Here’s a nonsensical
example that customizes FedAvg
by adding a custom "hello": "world"
configuration
key/value pair to the config dict of a single client (only the first client in the
list, the other clients in this round to not receive this “special” config value):
class CustomClientConfigStrategy(fl.server.strategy.FedAvg):
def configure_fit(
self, server_round: int, parameters: Parameters, client_manager: ClientManager
) -> List[Tuple[ClientProxy, FitIns]]:
client_instructions = super().configure_fit(
server_round, parameters, client_manager
)
# Add special "hello": "world" config key/value pair,
# but only to the first client in the list
_, fit_ins = client_instructions[0] # First (ClientProxy, FitIns) pair
fit_ins.config["hello"] = "world" # Change config for this client only
return client_instructions
# Create strategy and run server
strategy = CustomClientConfigStrategy(
# ... (same arguments as plain FedAvg here)
)
fl.server.start_server(strategy=strategy)