Clients
Strategy-Based Client Architecture
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:
LifecycleStrategyprepares the datasource, trainer, and samplers.PayloadStrategyrebuilds inbound payloads and prepares outbound data.TrainingStrategyloads weights and runs the local optimisation loop.ReportingStrategyfinalises metadata and serves asynchronous requests.CommunicationStrategyserialises 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.
from plato.clients import base
from plato.clients.strategies import (
DefaultCommunicationStrategy,
DefaultLifecycleStrategy,
DefaultReportingStrategy,
DefaultTrainingStrategy,
)
from plato.clients.strategies.defaults import DefaultPayloadStrategy
class AugmentedPayloadStrategy(DefaultPayloadStrategy):
def outbound_ready(self, context, report, outbound_payload):
super().outbound_ready(context, report, outbound_payload)
report.extra_metrics = context.metadata.get("custom_metrics", {})
class VisionClient(base.Client):
def __init__(self, *, callbacks=None):
super().__init__(callbacks=callbacks)
self._configure_composable(
lifecycle_strategy=DefaultLifecycleStrategy(),
payload_strategy=AugmentedPayloadStrategy(),
training_strategy=DefaultTrainingStrategy(),
reporting_strategy=DefaultReportingStrategy(),
communication_strategy=DefaultCommunicationStrategy(),
)
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, orcontext.trainsetduringLifecycleStrategy.allocate_data. - Share intermediate values via
context.stateand expose round metadata throughcontext.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).
Strategy Extension Points
LifecycleStrategy(plato/clients/strategies/base.py) governs configuration. Override: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 asclients.do_test. -
PayloadStrategycoordinates payload reconstruction. Reuse the default for pickled model weights, or override: accumulate_chunk/commit_chunk_groupfor multi-part transfers.finalise_inbound_payloadwhen downloading from external storage (S3, split learning, etc.).-
handle_server_payloadto apply custom preprocessing before training. -
TrainingStrategyencapsulates weight loading and the local optimisation loop. Implementload_payloadandtrain; the default delegates to the configured algorithm and trainer while respecting optional evaluation (clients.do_test,clients.test_interval). -
ReportingStrategyfinalises metadata. Overridebuild_reportto enrich the report before it leaves the client, or customiseobtain_model_at_timeto serve asynchronous updates. -
CommunicationStrategyhandles 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 replacingsend_reportandsend_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, andAFLReportingStrategy(underexamples/client_selection/) compute statistical utility or valuations insidebuild_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, whileFedNovaLifecycleStrategy(examples/server_aggregation/fednova/fednova_client.py) performs per-client RNG seeding duringconfigure. - Training specialisation. Strategies such as
DLGTrainingStrategy(examples/gradient_leakage_attacks/dlg_client.py),SubFedAvgTrainingStrategy(examples/model_pruning/sub_fedavg/subfedavg_client.py), andFedSawTrainingStrategy/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: seeCsMamlEdgeTrainingStrategyandFedSawEdgeTrainingStrategyfor 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.
on_inbound_received()
def on_inbound_received(self, client, inbound_processor)
Override this method to complete additional tasks before the inbound processors start to process the data received from the server.
inbound_processor the pipeline of inbound processors. The list of inbound processor instances can be accessed through its attribute 'processors'.
on_inbound_processed()
def on_inbound_processed(self, client, data)
Override this method to complete additional tasks when the inbound data has been processed by inbound processors.
data the inbound data after being processed by inbound processors, e.g., model weights before loaded to the trainer.
Example:
def on_inbound_processed(self, client, data):
# print the layer names of the model weights before further operations
for name, weights in data:
print(name)
on_outbound_ready()
def on_outbound_ready(self, client, outbound_processor)
Override this method to complete additional tasks before the outbound processors start to process the data to be sent to the server.
outbound_processor the pipeline of outbound processors. The list of inbound processor instances can be accessed through its attribute 'processors'.