Plato's client runtime now centres on a composable pipeline rather than deeply
nested subclasses. Every plato.clients.base.Client instance owns a
ComposableClient (plato/clients/composable.py) that orchestrates five
pluggable strategies:
LifecycleStrategy prepares the datasource, trainer, and samplers.
PayloadStrategy rebuilds inbound payloads and prepares outbound data.
TrainingStrategy loads weights and runs the local optimisation loop.
ReportingStrategy finalises metadata and serves asynchronous requests.
CommunicationStrategy serialises reports/payloads for transport.
Shared state flows between these strategies through ClientContext
(plato/clients/strategies/base.py). The context mirrors historically mutable
attributes—client id, datasource, processors, timers, and callbacks—so the
strategies can collaborate without touching private attributes on the client.
The default stack (Default*Strategy in
plato/clients/strategies/defaults.py) reproduces the legacy behaviour that
powered simple.Client. Specialised presets build on top of the same base,
for example EdgeLifecycleStrategy.
Composing Clients
The reference implementation in plato/clients/simple.py illustrates how to
assemble a strategy-based client: configure custom factories on the context,
then call _configure_composable(...) with the desired strategy instances.
Only the strategies you swap need new code—inherit the defaults elsewhere.
Within a strategy you receive a ClientContext rather than the client
instance. This makes it straightforward to compose behaviour:
Inspect or mutate context.sampler, context.datasource, or
context.trainset during LifecycleStrategy.allocate_data.
Share intermediate values via context.state and expose round metadata
through context.metadata.
Call context.callback_handler.call_event(...) to reuse the existing
callback pipeline whenever you add new strategy events.
Remember to synchronise any long-lived fields back to the owner if you change
them in place (see ComposableClient._sync_owner_from_context for reference).
process_server_response(context, server_response) to populate round
metadata or react to scheduler hints.
load_data(context) to build datasources or skip them for proxy clients.
configure(context) to instantiate trainers/algorithms/processors.
allocate_data(context) to wire samplers and train/test partitions.
The defaults fetch registry components and honour config flags such as
clients.do_test.
PayloadStrategy coordinates payload reconstruction. Reuse the default
for pickled model weights, or override:
accumulate_chunk / commit_chunk_group for multi-part transfers.
finalise_inbound_payload when downloading from external storage (S3,
split learning, etc.).
handle_server_payload to apply custom preprocessing before training.
TrainingStrategy encapsulates weight loading and the local optimisation
loop. Implement load_payload and train; the default delegates to the
configured algorithm and trainer while respecting optional evaluation
(clients.do_test, clients.test_interval).
ReportingStrategy finalises metadata. Override build_report to enrich
the report before it leaves the client, or customise
obtain_model_at_time to serve asynchronous updates.
CommunicationStrategy handles transport. The default emits Socket.IO
events and optionally uploads to S3, but you can substitute a strategy for
alternative channels (file system, RPC, simulated environments) by replacing
send_report and send_payload.
Each strategy exposes optional setup/teardown hooks; use them to allocate
resources when the client boots or release them once the round finishes.
Strategy Recipes in Examples
Most example workloads now customise clients by swapping strategies instead of
overriding legacy hooks. The following patterns provide practical templates:
Reporting augmentations.OortReportingStrategy,
PiscesReportingStrategy, and AFLReportingStrategy (under
examples/client_selection/) compute statistical utility or valuations
inside build_report, pulling metrics from trainer run histories and
tolerating missing data.
Lifecycle configuration.ScaffoldLifecycleStrategy
(examples/customized_client_training/scaffold/scaffold_client.py) restores
persisted control variates, while FedNovaLifecycleStrategy
(examples/server_aggregation/fednova/fednova_client.py) performs per-client
RNG seeding during configure.
Training specialisation. Strategies such as DLGTrainingStrategy
(examples/gradient_leakage_attacks/dlg_client.py), SubFedAvgTrainingStrategy
(examples/model_pruning/sub_fedavg/subfedavg_client.py), and
FedSawTrainingStrategy / FedSawEdgeTrainingStrategy
(examples/three_layer_fl/fedsaw/) show how to augment payloads, add logging,
or prune updates before transmission. FlMamlTrainingStrategy
(examples/outdated/fl_maml/fl_maml_client.py) illustrates personalised
evaluation flows.
Metadata propagation. NAS and pruning case studies attach algorithm state
to reports via strategy overrides (FedRLNASReportingStrategy,
PerFedRLNASReportingStrategy, FedSCRReportingStrategy, etc.), eliminating
the need to mutate the client directly.
Edge coordination. Cross-silo scenarios extend
EdgeTrainingStrategy: see CsMamlEdgeTrainingStrategy and
FedSawEdgeTrainingStrategy for examples that add personalisation tests or
post-aggregation pruning.
Copy one of these strategies, tailor the hook you need, then wire it into
_configure_composable(...) on your client subclass.
Migration Notes
The legacy adapter layer (plato/clients/strategies/legacy.py) has been
removed. Client subclasses must configure strategies explicitly via
_configure_composable(...); overriding legacy hooks such as _train or
_load_payload no longer affects the runtime.
Client Callbacks
Callbacks remain the preferred way to inject cross-cutting concerns such as
logging, tracing, or metrics aggregation. Subclass
plato.callbacks.client.ClientCallback, implement the relevant
on_inbound_received, on_inbound_processed, or on_outbound_ready hooks,
and pass the callback class to the client constructor (or call
client.add_callbacks).
The callback handler is stored on ClientContext.callback_handler, so strategy
implementations can continue to fire the same events that legacy clients used.
When designing new strategies, invoke the handler to keep observability
features working for downstream experiments.
Customizing Clients using Callbacks
For infrastructure changes, such as logging and recording metrics, we tend to customize the client using callbacks instead. The advantage of using callbacks is that one can pass a list of multiple callbacks to the client when it is initialized, and they will be called in their order in the provided list. This helps when it is necessary to group features into different callback classes.
Within the implementation of these callback methods, one can access additional information about the local training by using the client instance.
To use callbacks, subclass the ClientCallback class in plato.callbacks.client, and override the following methods, then pass it to the client when it is initialized, or call client.add_callbacks after initialization. Examples can be found in examples/callbacks.