flwr-nightly 1.8.0.dev20240315__py3-none-any.whl → 1.11.0.dev20240813__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/app.py +7 -0
- flwr/cli/build.py +150 -0
- flwr/cli/config_utils.py +219 -0
- flwr/cli/example.py +3 -1
- flwr/cli/install.py +227 -0
- flwr/cli/new/new.py +179 -48
- flwr/cli/new/templates/app/.gitignore.tpl +160 -0
- flwr/cli/new/templates/app/README.flowertune.md.tpl +56 -0
- flwr/cli/new/templates/app/README.md.tpl +1 -5
- flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +65 -0
- flwr/cli/new/templates/app/code/client.jax.py.tpl +56 -0
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +93 -0
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +3 -2
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +23 -11
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +97 -0
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +60 -1
- flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
- flwr/cli/new/templates/app/code/flwr_tune/app.py.tpl +89 -0
- flwr/cli/new/templates/app/code/flwr_tune/client.py.tpl +126 -0
- flwr/cli/new/templates/app/code/flwr_tune/config.yaml.tpl +34 -0
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +57 -0
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +59 -0
- flwr/cli/new/templates/app/code/flwr_tune/server.py.tpl +48 -0
- flwr/cli/new/templates/app/code/flwr_tune/static_config.yaml.tpl +11 -0
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -0
- flwr/cli/new/templates/app/code/server.jax.py.tpl +20 -0
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +20 -0
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +17 -9
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +24 -0
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +99 -0
- flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +28 -23
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +39 -0
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +38 -0
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +34 -0
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +33 -0
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
- flwr/cli/run/run.py +168 -17
- flwr/cli/utils.py +75 -4
- flwr/client/__init__.py +6 -1
- flwr/client/app.py +239 -248
- flwr/client/client_app.py +70 -9
- flwr/client/dpfedavg_numpy_client.py +1 -1
- flwr/client/grpc_adapter_client/__init__.py +15 -0
- flwr/client/grpc_adapter_client/connection.py +97 -0
- flwr/client/grpc_client/connection.py +18 -5
- flwr/client/grpc_rere_client/__init__.py +1 -1
- flwr/client/grpc_rere_client/client_interceptor.py +158 -0
- flwr/client/grpc_rere_client/connection.py +127 -33
- flwr/client/grpc_rere_client/grpc_adapter.py +140 -0
- flwr/client/heartbeat.py +74 -0
- flwr/client/message_handler/__init__.py +1 -1
- flwr/client/message_handler/message_handler.py +7 -7
- flwr/client/mod/__init__.py +5 -5
- flwr/client/mod/centraldp_mods.py +4 -2
- flwr/client/mod/comms_mods.py +4 -4
- flwr/client/mod/localdp_mod.py +9 -4
- flwr/client/mod/secure_aggregation/__init__.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/mod/utils.py +1 -1
- flwr/client/node_state.py +60 -10
- flwr/client/node_state_tests.py +4 -3
- flwr/client/rest_client/__init__.py +1 -1
- flwr/client/rest_client/connection.py +177 -157
- flwr/client/supernode/__init__.py +26 -0
- flwr/client/supernode/app.py +464 -0
- flwr/client/typing.py +1 -0
- flwr/common/__init__.py +13 -11
- flwr/common/address.py +1 -1
- flwr/common/config.py +193 -0
- flwr/common/constant.py +42 -1
- flwr/common/context.py +26 -1
- flwr/common/date.py +1 -1
- flwr/common/dp.py +1 -1
- flwr/common/grpc.py +6 -2
- flwr/common/logger.py +79 -8
- flwr/common/message.py +167 -105
- flwr/common/object_ref.py +126 -25
- flwr/common/record/__init__.py +1 -1
- flwr/common/record/parametersrecord.py +0 -1
- flwr/common/record/recordset.py +78 -27
- flwr/common/recordset_compat.py +8 -1
- flwr/common/retry_invoker.py +25 -13
- flwr/common/secure_aggregation/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/shamir.py +1 -1
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +21 -2
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +1 -1
- flwr/common/secure_aggregation/quantization.py +1 -1
- flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
- flwr/common/secure_aggregation/secaggplus_utils.py +1 -1
- flwr/common/serde.py +209 -3
- flwr/common/telemetry.py +25 -0
- flwr/common/typing.py +38 -0
- flwr/common/version.py +14 -0
- flwr/proto/clientappio_pb2.py +41 -0
- flwr/proto/clientappio_pb2.pyi +110 -0
- flwr/proto/clientappio_pb2_grpc.py +101 -0
- flwr/proto/clientappio_pb2_grpc.pyi +40 -0
- flwr/proto/common_pb2.py +36 -0
- flwr/proto/common_pb2.pyi +121 -0
- flwr/proto/common_pb2_grpc.py +4 -0
- flwr/proto/common_pb2_grpc.pyi +4 -0
- flwr/proto/driver_pb2.py +26 -19
- flwr/proto/driver_pb2.pyi +34 -0
- flwr/proto/driver_pb2_grpc.py +70 -0
- flwr/proto/driver_pb2_grpc.pyi +28 -0
- flwr/proto/exec_pb2.py +43 -0
- flwr/proto/exec_pb2.pyi +95 -0
- flwr/proto/exec_pb2_grpc.py +101 -0
- flwr/proto/exec_pb2_grpc.pyi +41 -0
- flwr/proto/fab_pb2.py +30 -0
- flwr/proto/fab_pb2.pyi +56 -0
- flwr/proto/fab_pb2_grpc.py +4 -0
- flwr/proto/fab_pb2_grpc.pyi +4 -0
- flwr/proto/fleet_pb2.py +29 -23
- flwr/proto/fleet_pb2.pyi +33 -0
- flwr/proto/fleet_pb2_grpc.py +102 -0
- flwr/proto/fleet_pb2_grpc.pyi +35 -0
- flwr/proto/grpcadapter_pb2.py +32 -0
- flwr/proto/grpcadapter_pb2.pyi +43 -0
- flwr/proto/grpcadapter_pb2_grpc.py +66 -0
- flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
- flwr/proto/message_pb2.py +41 -0
- flwr/proto/message_pb2.pyi +122 -0
- flwr/proto/message_pb2_grpc.py +4 -0
- flwr/proto/message_pb2_grpc.pyi +4 -0
- flwr/proto/run_pb2.py +35 -0
- flwr/proto/run_pb2.pyi +76 -0
- flwr/proto/run_pb2_grpc.py +4 -0
- flwr/proto/run_pb2_grpc.pyi +4 -0
- flwr/proto/task_pb2.py +7 -8
- flwr/proto/task_pb2.pyi +8 -5
- flwr/server/__init__.py +4 -8
- flwr/server/app.py +298 -350
- flwr/server/compat/app.py +6 -57
- flwr/server/compat/app_utils.py +5 -4
- flwr/server/compat/driver_client_proxy.py +29 -48
- flwr/server/compat/legacy_context.py +5 -4
- flwr/server/driver/__init__.py +2 -0
- flwr/server/driver/driver.py +22 -132
- flwr/server/driver/grpc_driver.py +224 -74
- flwr/server/driver/inmemory_driver.py +183 -0
- flwr/server/history.py +20 -20
- flwr/server/run_serverapp.py +121 -34
- flwr/server/server.py +11 -7
- flwr/server/server_app.py +59 -10
- flwr/server/serverapp_components.py +52 -0
- flwr/server/strategy/__init__.py +2 -2
- flwr/server/strategy/bulyan.py +1 -1
- flwr/server/strategy/dp_adaptive_clipping.py +3 -3
- flwr/server/strategy/dp_fixed_clipping.py +4 -3
- flwr/server/strategy/dpfedavg_adaptive.py +1 -1
- flwr/server/strategy/dpfedavg_fixed.py +1 -1
- flwr/server/strategy/fedadagrad.py +1 -1
- flwr/server/strategy/fedadam.py +1 -1
- flwr/server/strategy/fedavg_android.py +1 -1
- flwr/server/strategy/fedavgm.py +1 -1
- flwr/server/strategy/fedmedian.py +1 -1
- flwr/server/strategy/fedopt.py +1 -1
- flwr/server/strategy/fedprox.py +1 -1
- flwr/server/strategy/fedxgb_bagging.py +1 -1
- flwr/server/strategy/fedxgb_cyclic.py +1 -1
- flwr/server/strategy/fedxgb_nn_avg.py +1 -1
- flwr/server/strategy/fedyogi.py +1 -1
- flwr/server/strategy/krum.py +1 -1
- flwr/server/strategy/qfedavg.py +1 -1
- flwr/server/superlink/driver/__init__.py +1 -1
- flwr/server/superlink/driver/driver_grpc.py +1 -1
- flwr/server/superlink/driver/driver_servicer.py +51 -4
- flwr/server/superlink/ffs/__init__.py +24 -0
- flwr/server/superlink/ffs/disk_ffs.py +104 -0
- flwr/server/superlink/ffs/ffs.py +79 -0
- flwr/server/superlink/fleet/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +131 -0
- flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +8 -2
- flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +30 -2
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +214 -0
- flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/rest_rere/rest_api.py +59 -1
- flwr/server/superlink/fleet/vce/backend/__init__.py +1 -1
- flwr/server/superlink/fleet/vce/backend/backend.py +5 -5
- flwr/server/superlink/fleet/vce/backend/raybackend.py +53 -56
- flwr/server/superlink/fleet/vce/vce_api.py +190 -127
- flwr/server/superlink/state/__init__.py +1 -1
- flwr/server/superlink/state/in_memory_state.py +159 -42
- flwr/server/superlink/state/sqlite_state.py +243 -39
- flwr/server/superlink/state/state.py +81 -6
- flwr/server/superlink/state/state_factory.py +11 -2
- flwr/server/superlink/state/utils.py +62 -0
- flwr/server/typing.py +2 -0
- flwr/server/utils/__init__.py +1 -1
- flwr/server/utils/tensorboard.py +1 -1
- flwr/server/utils/validator.py +23 -9
- flwr/server/workflow/default_workflows.py +67 -25
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +18 -6
- flwr/simulation/__init__.py +7 -4
- flwr/simulation/app.py +67 -36
- flwr/simulation/ray_transport/__init__.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +20 -46
- flwr/simulation/ray_transport/ray_client_proxy.py +36 -16
- flwr/simulation/run_simulation.py +308 -92
- flwr/superexec/__init__.py +21 -0
- flwr/superexec/app.py +184 -0
- flwr/superexec/deployment.py +185 -0
- flwr/superexec/exec_grpc.py +55 -0
- flwr/superexec/exec_servicer.py +70 -0
- flwr/superexec/executor.py +75 -0
- flwr/superexec/simulation.py +193 -0
- {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/METADATA +10 -6
- flwr_nightly-1.11.0.dev20240813.dist-info/RECORD +288 -0
- flwr_nightly-1.11.0.dev20240813.dist-info/entry_points.txt +10 -0
- flwr/cli/flower_toml.py +0 -140
- flwr/cli/new/templates/app/flower.toml.tpl +0 -13
- flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
- flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
- flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
- flwr_nightly-1.8.0.dev20240315.dist-info/RECORD +0 -211
- flwr_nightly-1.8.0.dev20240315.dist-info/entry_points.txt +0 -9
- {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.8.0.dev20240315.dist-info → flwr_nightly-1.11.0.dev20240813.dist-info}/WHEEL +0 -0
flwr/client/app.py
CHANGED
|
@@ -14,33 +14,37 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower client app."""
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
import argparse
|
|
17
|
+
import signal
|
|
19
18
|
import sys
|
|
20
19
|
import time
|
|
21
|
-
from
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from logging import ERROR, INFO, WARN
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Callable, ContextManager, Optional, Tuple, Type, Union
|
|
23
|
+
from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
|
|
24
24
|
|
|
25
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
25
26
|
from grpc import RpcError
|
|
26
27
|
|
|
27
28
|
from flwr.client.client import Client
|
|
28
29
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
29
|
-
from flwr.client.typing import
|
|
30
|
-
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, Message, event
|
|
30
|
+
from flwr.client.typing import ClientFnExt
|
|
31
|
+
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
|
|
31
32
|
from flwr.common.address import parse_address
|
|
32
33
|
from flwr.common.constant import (
|
|
33
34
|
MISSING_EXTRA_REST,
|
|
35
|
+
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
34
36
|
TRANSPORT_TYPE_GRPC_BIDI,
|
|
35
37
|
TRANSPORT_TYPE_GRPC_RERE,
|
|
36
38
|
TRANSPORT_TYPE_REST,
|
|
37
39
|
TRANSPORT_TYPES,
|
|
40
|
+
ErrorCode,
|
|
38
41
|
)
|
|
39
|
-
from flwr.common.
|
|
40
|
-
from flwr.common.
|
|
41
|
-
from flwr.common.
|
|
42
|
-
from flwr.common.
|
|
42
|
+
from flwr.common.logger import log, warn_deprecated_feature
|
|
43
|
+
from flwr.common.message import Error
|
|
44
|
+
from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
|
|
45
|
+
from flwr.common.typing import Fab, Run, UserConfig
|
|
43
46
|
|
|
47
|
+
from .grpc_adapter_client.connection import grpc_adapter
|
|
44
48
|
from .grpc_client.connection import grpc_connection
|
|
45
49
|
from .grpc_rere_client.connection import grpc_request_response
|
|
46
50
|
from .message_handler.message_handler import handle_control_message
|
|
@@ -48,144 +52,8 @@ from .node_state import NodeState
|
|
|
48
52
|
from .numpy_client import NumPyClient
|
|
49
53
|
|
|
50
54
|
|
|
51
|
-
def run_client_app() -> None:
|
|
52
|
-
"""Run Flower client app."""
|
|
53
|
-
event(EventType.RUN_CLIENT_APP_ENTER)
|
|
54
|
-
|
|
55
|
-
log(INFO, "Long-running Flower client starting")
|
|
56
|
-
|
|
57
|
-
args = _parse_args_run_client_app().parse_args()
|
|
58
|
-
|
|
59
|
-
# Obtain certificates
|
|
60
|
-
if args.insecure:
|
|
61
|
-
if args.root_certificates is not None:
|
|
62
|
-
sys.exit(
|
|
63
|
-
"Conflicting options: The '--insecure' flag disables HTTPS, "
|
|
64
|
-
"but '--root-certificates' was also specified. Please remove "
|
|
65
|
-
"the '--root-certificates' option when running in insecure mode, "
|
|
66
|
-
"or omit '--insecure' to use HTTPS."
|
|
67
|
-
)
|
|
68
|
-
log(
|
|
69
|
-
WARN,
|
|
70
|
-
"Option `--insecure` was set. "
|
|
71
|
-
"Starting insecure HTTP client connected to %s.",
|
|
72
|
-
args.server,
|
|
73
|
-
)
|
|
74
|
-
root_certificates = None
|
|
75
|
-
else:
|
|
76
|
-
# Load the certificates if provided, or load the system certificates
|
|
77
|
-
cert_path = args.root_certificates
|
|
78
|
-
if cert_path is None:
|
|
79
|
-
root_certificates = None
|
|
80
|
-
else:
|
|
81
|
-
root_certificates = Path(cert_path).read_bytes()
|
|
82
|
-
log(
|
|
83
|
-
DEBUG,
|
|
84
|
-
"Starting secure HTTPS client connected to %s "
|
|
85
|
-
"with the following certificates: %s.",
|
|
86
|
-
args.server,
|
|
87
|
-
cert_path,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
log(
|
|
91
|
-
DEBUG,
|
|
92
|
-
"Flower will load ClientApp `%s`",
|
|
93
|
-
getattr(args, "client-app"),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
client_app_dir = args.dir
|
|
97
|
-
if client_app_dir is not None:
|
|
98
|
-
sys.path.insert(0, client_app_dir)
|
|
99
|
-
|
|
100
|
-
app_ref: str = getattr(args, "client-app")
|
|
101
|
-
valid, error_msg = validate(app_ref)
|
|
102
|
-
if not valid and error_msg:
|
|
103
|
-
raise LoadClientAppError(error_msg) from None
|
|
104
|
-
|
|
105
|
-
def _load() -> ClientApp:
|
|
106
|
-
client_app = load_app(app_ref, LoadClientAppError)
|
|
107
|
-
|
|
108
|
-
if not isinstance(client_app, ClientApp):
|
|
109
|
-
raise LoadClientAppError(
|
|
110
|
-
f"Attribute {app_ref} is not of type {ClientApp}",
|
|
111
|
-
) from None
|
|
112
|
-
|
|
113
|
-
return client_app
|
|
114
|
-
|
|
115
|
-
_start_client_internal(
|
|
116
|
-
server_address=args.server,
|
|
117
|
-
load_client_app_fn=_load,
|
|
118
|
-
transport="rest" if args.rest else "grpc-rere",
|
|
119
|
-
root_certificates=root_certificates,
|
|
120
|
-
insecure=args.insecure,
|
|
121
|
-
max_retries=args.max_retries,
|
|
122
|
-
max_wait_time=args.max_wait_time,
|
|
123
|
-
)
|
|
124
|
-
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _parse_args_run_client_app() -> argparse.ArgumentParser:
|
|
128
|
-
"""Parse flower-client-app command line arguments."""
|
|
129
|
-
parser = argparse.ArgumentParser(
|
|
130
|
-
description="Start a Flower client app",
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
parser.add_argument(
|
|
134
|
-
"client-app",
|
|
135
|
-
help="For example: `client:app` or `project.package.module:wrapper.app`",
|
|
136
|
-
)
|
|
137
|
-
parser.add_argument(
|
|
138
|
-
"--insecure",
|
|
139
|
-
action="store_true",
|
|
140
|
-
help="Run the client without HTTPS. By default, the client runs with "
|
|
141
|
-
"HTTPS enabled. Use this flag only if you understand the risks.",
|
|
142
|
-
)
|
|
143
|
-
parser.add_argument(
|
|
144
|
-
"--rest",
|
|
145
|
-
action="store_true",
|
|
146
|
-
help="Use REST as a transport layer for the client.",
|
|
147
|
-
)
|
|
148
|
-
parser.add_argument(
|
|
149
|
-
"--root-certificates",
|
|
150
|
-
metavar="ROOT_CERT",
|
|
151
|
-
type=str,
|
|
152
|
-
help="Specifies the path to the PEM-encoded root certificate file for "
|
|
153
|
-
"establishing secure HTTPS connections.",
|
|
154
|
-
)
|
|
155
|
-
parser.add_argument(
|
|
156
|
-
"--server",
|
|
157
|
-
default="0.0.0.0:9092",
|
|
158
|
-
help="Server address",
|
|
159
|
-
)
|
|
160
|
-
parser.add_argument(
|
|
161
|
-
"--max-retries",
|
|
162
|
-
type=int,
|
|
163
|
-
default=None,
|
|
164
|
-
help="The maximum number of times the client will try to connect to the"
|
|
165
|
-
"server before giving up in case of a connection error. By default,"
|
|
166
|
-
"it is set to None, meaning there is no limit to the number of tries.",
|
|
167
|
-
)
|
|
168
|
-
parser.add_argument(
|
|
169
|
-
"--max-wait-time",
|
|
170
|
-
type=float,
|
|
171
|
-
default=None,
|
|
172
|
-
help="The maximum duration before the client stops trying to"
|
|
173
|
-
"connect to the server in case of connection error. By default, it"
|
|
174
|
-
"is set to None, meaning there is no limit to the total time.",
|
|
175
|
-
)
|
|
176
|
-
parser.add_argument(
|
|
177
|
-
"--dir",
|
|
178
|
-
default="",
|
|
179
|
-
help="Add specified directory to the PYTHONPATH and load Flower "
|
|
180
|
-
"app from there."
|
|
181
|
-
" Default: current working directory.",
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
return parser
|
|
185
|
-
|
|
186
|
-
|
|
187
55
|
def _check_actionable_client(
|
|
188
|
-
client: Optional[Client], client_fn: Optional[
|
|
56
|
+
client: Optional[Client], client_fn: Optional[ClientFnExt]
|
|
189
57
|
) -> None:
|
|
190
58
|
if client_fn is None and client is None:
|
|
191
59
|
raise ValueError(
|
|
@@ -206,12 +74,15 @@ def _check_actionable_client(
|
|
|
206
74
|
def start_client(
|
|
207
75
|
*,
|
|
208
76
|
server_address: str,
|
|
209
|
-
client_fn: Optional[
|
|
77
|
+
client_fn: Optional[ClientFnExt] = None,
|
|
210
78
|
client: Optional[Client] = None,
|
|
211
79
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
|
212
80
|
root_certificates: Optional[Union[bytes, str]] = None,
|
|
213
81
|
insecure: Optional[bool] = None,
|
|
214
82
|
transport: Optional[str] = None,
|
|
83
|
+
authentication_keys: Optional[
|
|
84
|
+
Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
|
85
|
+
] = None,
|
|
215
86
|
max_retries: Optional[int] = None,
|
|
216
87
|
max_wait_time: Optional[float] = None,
|
|
217
88
|
) -> None:
|
|
@@ -223,7 +94,7 @@ def start_client(
|
|
|
223
94
|
The IPv4 or IPv6 address of the server. If the Flower
|
|
224
95
|
server runs on the same machine on port 8080, then `server_address`
|
|
225
96
|
would be `"[::]:8080"`.
|
|
226
|
-
client_fn : Optional[
|
|
97
|
+
client_fn : Optional[ClientFnExt]
|
|
227
98
|
A callable that instantiates a Client. (default: None)
|
|
228
99
|
client : Optional[flwr.client.Client]
|
|
229
100
|
An implementation of the abstract base
|
|
@@ -267,8 +138,8 @@ def start_client(
|
|
|
267
138
|
|
|
268
139
|
Starting an SSL-enabled gRPC client using system certificates:
|
|
269
140
|
|
|
270
|
-
>>> def client_fn(
|
|
271
|
-
>>> return FlowerClient()
|
|
141
|
+
>>> def client_fn(context: Context):
|
|
142
|
+
>>> return FlowerClient().to_client()
|
|
272
143
|
>>>
|
|
273
144
|
>>> start_client(
|
|
274
145
|
>>> server_address=localhost:8080,
|
|
@@ -289,6 +160,7 @@ def start_client(
|
|
|
289
160
|
event(EventType.START_CLIENT_ENTER)
|
|
290
161
|
_start_client_internal(
|
|
291
162
|
server_address=server_address,
|
|
163
|
+
node_config={},
|
|
292
164
|
load_client_app_fn=None,
|
|
293
165
|
client_fn=client_fn,
|
|
294
166
|
client=client,
|
|
@@ -296,6 +168,7 @@ def start_client(
|
|
|
296
168
|
root_certificates=root_certificates,
|
|
297
169
|
insecure=insecure,
|
|
298
170
|
transport=transport,
|
|
171
|
+
authentication_keys=authentication_keys,
|
|
299
172
|
max_retries=max_retries,
|
|
300
173
|
max_wait_time=max_wait_time,
|
|
301
174
|
)
|
|
@@ -309,15 +182,20 @@ def start_client(
|
|
|
309
182
|
def _start_client_internal(
|
|
310
183
|
*,
|
|
311
184
|
server_address: str,
|
|
312
|
-
|
|
313
|
-
|
|
185
|
+
node_config: UserConfig,
|
|
186
|
+
load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
|
|
187
|
+
client_fn: Optional[ClientFnExt] = None,
|
|
314
188
|
client: Optional[Client] = None,
|
|
315
189
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
|
316
190
|
root_certificates: Optional[Union[bytes, str]] = None,
|
|
317
191
|
insecure: Optional[bool] = None,
|
|
318
192
|
transport: Optional[str] = None,
|
|
193
|
+
authentication_keys: Optional[
|
|
194
|
+
Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
|
195
|
+
] = None,
|
|
319
196
|
max_retries: Optional[int] = None,
|
|
320
197
|
max_wait_time: Optional[float] = None,
|
|
198
|
+
flwr_path: Optional[Path] = None,
|
|
321
199
|
) -> None:
|
|
322
200
|
"""Start a Flower client node which connects to a Flower server.
|
|
323
201
|
|
|
@@ -327,9 +205,11 @@ def _start_client_internal(
|
|
|
327
205
|
The IPv4 or IPv6 address of the server. If the Flower
|
|
328
206
|
server runs on the same machine on port 8080, then `server_address`
|
|
329
207
|
would be `"[::]:8080"`.
|
|
208
|
+
node_config: UserConfig
|
|
209
|
+
The configuration of the node.
|
|
330
210
|
load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
|
|
331
211
|
A function that can be used to load a `ClientApp` instance.
|
|
332
|
-
client_fn : Optional[
|
|
212
|
+
client_fn : Optional[ClientFnExt]
|
|
333
213
|
A callable that instantiates a Client. (default: None)
|
|
334
214
|
client : Optional[flwr.client.Client]
|
|
335
215
|
An implementation of the abstract base
|
|
@@ -361,6 +241,8 @@ def _start_client_internal(
|
|
|
361
241
|
The maximum duration before the client stops trying to
|
|
362
242
|
connect to the server in case of connection error.
|
|
363
243
|
If set to None, there is no limit to the total time.
|
|
244
|
+
flwr_path: Optional[Path] (default: None)
|
|
245
|
+
The fully resolved path containing installed Flower Apps.
|
|
364
246
|
"""
|
|
365
247
|
if insecure is None:
|
|
366
248
|
insecure = root_certificates is None
|
|
@@ -371,7 +253,7 @@ def _start_client_internal(
|
|
|
371
253
|
if client_fn is None:
|
|
372
254
|
# Wrap `Client` instance in `client_fn`
|
|
373
255
|
def single_client_factory(
|
|
374
|
-
|
|
256
|
+
context: Context, # pylint: disable=unused-argument
|
|
375
257
|
) -> Client:
|
|
376
258
|
if client is None: # Added this to keep mypy happy
|
|
377
259
|
raise ValueError(
|
|
@@ -381,12 +263,10 @@ def _start_client_internal(
|
|
|
381
263
|
|
|
382
264
|
client_fn = single_client_factory
|
|
383
265
|
|
|
384
|
-
def _load_client_app() -> ClientApp:
|
|
266
|
+
def _load_client_app(_1: str, _2: str) -> ClientApp:
|
|
385
267
|
return ClientApp(client_fn=client_fn)
|
|
386
268
|
|
|
387
269
|
load_client_app_fn = _load_client_app
|
|
388
|
-
else:
|
|
389
|
-
warn_experimental_feature("`load_client_app_fn`")
|
|
390
270
|
|
|
391
271
|
# At this point, only `load_client_app_fn` should be used
|
|
392
272
|
# Both `client` and `client_fn` must not be used directly
|
|
@@ -396,10 +276,33 @@ def _start_client_internal(
|
|
|
396
276
|
transport, server_address
|
|
397
277
|
)
|
|
398
278
|
|
|
279
|
+
app_state_tracker = _AppStateTracker()
|
|
280
|
+
|
|
281
|
+
def _on_sucess(retry_state: RetryState) -> None:
|
|
282
|
+
app_state_tracker.is_connected = True
|
|
283
|
+
if retry_state.tries > 1:
|
|
284
|
+
log(
|
|
285
|
+
INFO,
|
|
286
|
+
"Connection successful after %.2f seconds and %s tries.",
|
|
287
|
+
retry_state.elapsed_time,
|
|
288
|
+
retry_state.tries,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _on_backoff(retry_state: RetryState) -> None:
|
|
292
|
+
app_state_tracker.is_connected = False
|
|
293
|
+
if retry_state.tries == 1:
|
|
294
|
+
log(WARN, "Connection attempt failed, retrying...")
|
|
295
|
+
else:
|
|
296
|
+
log(
|
|
297
|
+
WARN,
|
|
298
|
+
"Connection attempt failed, retrying in %.2f seconds",
|
|
299
|
+
retry_state.actual_wait,
|
|
300
|
+
)
|
|
301
|
+
|
|
399
302
|
retry_invoker = RetryInvoker(
|
|
400
|
-
|
|
303
|
+
wait_gen_factory=exponential,
|
|
401
304
|
recoverable_exceptions=connection_error_type,
|
|
402
|
-
max_tries=max_retries,
|
|
305
|
+
max_tries=max_retries + 1 if max_retries is not None else None,
|
|
403
306
|
max_time=max_wait_time,
|
|
404
307
|
on_giveup=lambda retry_state: (
|
|
405
308
|
log(
|
|
@@ -411,30 +314,16 @@ def _start_client_internal(
|
|
|
411
314
|
if retry_state.tries > 1
|
|
412
315
|
else None
|
|
413
316
|
),
|
|
414
|
-
on_success=
|
|
415
|
-
|
|
416
|
-
INFO,
|
|
417
|
-
"Connection successful after %.2f seconds and %s tries.",
|
|
418
|
-
retry_state.elapsed_time,
|
|
419
|
-
retry_state.tries,
|
|
420
|
-
)
|
|
421
|
-
if retry_state.tries > 1
|
|
422
|
-
else None
|
|
423
|
-
),
|
|
424
|
-
on_backoff=lambda retry_state: (
|
|
425
|
-
log(WARN, "Connection attempt failed, retrying...")
|
|
426
|
-
if retry_state.tries == 1
|
|
427
|
-
else log(
|
|
428
|
-
DEBUG,
|
|
429
|
-
"Connection attempt failed, retrying in %.2f seconds",
|
|
430
|
-
retry_state.actual_wait,
|
|
431
|
-
)
|
|
432
|
-
),
|
|
317
|
+
on_success=_on_sucess,
|
|
318
|
+
on_backoff=_on_backoff,
|
|
433
319
|
)
|
|
434
320
|
|
|
435
|
-
|
|
321
|
+
# NodeState gets initialized when the first connection is established
|
|
322
|
+
node_state: Optional[NodeState] = None
|
|
323
|
+
|
|
324
|
+
runs: Dict[int, Run] = {}
|
|
436
325
|
|
|
437
|
-
while
|
|
326
|
+
while not app_state_tracker.interrupt:
|
|
438
327
|
sleep_duration: int = 0
|
|
439
328
|
with connection(
|
|
440
329
|
address,
|
|
@@ -442,80 +331,154 @@ def _start_client_internal(
|
|
|
442
331
|
retry_invoker,
|
|
443
332
|
grpc_max_message_length,
|
|
444
333
|
root_certificates,
|
|
334
|
+
authentication_keys,
|
|
445
335
|
) as conn:
|
|
446
|
-
receive, send, create_node, delete_node = conn
|
|
447
|
-
|
|
448
|
-
# Register node
|
|
449
|
-
if
|
|
450
|
-
create_node
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
336
|
+
receive, send, create_node, delete_node, get_run, _ = conn
|
|
337
|
+
|
|
338
|
+
# Register node when connecting the first time
|
|
339
|
+
if node_state is None:
|
|
340
|
+
if create_node is None:
|
|
341
|
+
if transport not in ["grpc-bidi", None]:
|
|
342
|
+
raise NotImplementedError(
|
|
343
|
+
"All transports except `grpc-bidi` require "
|
|
344
|
+
"an implementation for `create_node()`.'"
|
|
345
|
+
)
|
|
346
|
+
# gRPC-bidi doesn't have the concept of node_id,
|
|
347
|
+
# so we set it to -1
|
|
348
|
+
node_state = NodeState(
|
|
349
|
+
node_id=-1,
|
|
350
|
+
node_config={},
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
# Call create_node fn to register node
|
|
354
|
+
node_id: Optional[int] = ( # pylint: disable=assignment-from-none
|
|
355
|
+
create_node()
|
|
356
|
+
) # pylint: disable=not-callable
|
|
357
|
+
if node_id is None:
|
|
358
|
+
raise ValueError("Node registration failed")
|
|
359
|
+
node_state = NodeState(
|
|
360
|
+
node_id=node_id,
|
|
361
|
+
node_config=node_config,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
app_state_tracker.register_signal_handler()
|
|
365
|
+
while not app_state_tracker.interrupt:
|
|
366
|
+
try:
|
|
367
|
+
# Receive
|
|
368
|
+
message = receive()
|
|
369
|
+
if message is None:
|
|
370
|
+
time.sleep(3) # Wait for 3s before asking again
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
log(INFO, "")
|
|
374
|
+
if len(message.metadata.group_id) > 0:
|
|
375
|
+
log(
|
|
376
|
+
INFO,
|
|
377
|
+
"[RUN %s, ROUND %s]",
|
|
378
|
+
message.metadata.run_id,
|
|
379
|
+
message.metadata.group_id,
|
|
380
|
+
)
|
|
381
|
+
log(
|
|
382
|
+
INFO,
|
|
383
|
+
"Received: %s message %s",
|
|
384
|
+
message.metadata.message_type,
|
|
385
|
+
message.metadata.message_id,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Handle control message
|
|
389
|
+
out_message, sleep_duration = handle_control_message(message)
|
|
390
|
+
if out_message:
|
|
391
|
+
send(out_message)
|
|
392
|
+
break
|
|
393
|
+
|
|
394
|
+
# Get run info
|
|
395
|
+
run_id = message.metadata.run_id
|
|
396
|
+
if run_id not in runs:
|
|
397
|
+
if get_run is not None:
|
|
398
|
+
runs[run_id] = get_run(run_id)
|
|
399
|
+
# If get_run is None, i.e., in grpc-bidi mode
|
|
400
|
+
else:
|
|
401
|
+
runs[run_id] = Run(run_id, "", "", {})
|
|
402
|
+
|
|
403
|
+
# Register context for this run
|
|
404
|
+
node_state.register_context(
|
|
405
|
+
run_id=run_id, run=runs[run_id], flwr_path=flwr_path
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Retrieve context for this run
|
|
409
|
+
context = node_state.retrieve_context(run_id=run_id)
|
|
478
410
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
411
|
+
# Create an error reply message that will never be used to prevent
|
|
412
|
+
# the used-before-assignment linting error
|
|
413
|
+
reply_message = message.create_error_reply(
|
|
414
|
+
error=Error(code=ErrorCode.UNKNOWN, reason="Unknown")
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Handle app loading and task message
|
|
418
|
+
try:
|
|
419
|
+
# Load ClientApp instance
|
|
420
|
+
run: Run = runs[run_id]
|
|
421
|
+
client_app: ClientApp = load_client_app_fn(
|
|
422
|
+
run.fab_id, run.fab_version
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Execute ClientApp
|
|
426
|
+
reply_message = client_app(message=message, context=context)
|
|
427
|
+
except Exception as ex: # pylint: disable=broad-exception-caught
|
|
428
|
+
|
|
429
|
+
# Legacy grpc-bidi
|
|
430
|
+
if transport in ["grpc-bidi", None]:
|
|
431
|
+
log(ERROR, "Client raised an exception.", exc_info=ex)
|
|
432
|
+
# Raise exception, crash process
|
|
433
|
+
raise ex
|
|
434
|
+
|
|
435
|
+
# Don't update/change NodeState
|
|
436
|
+
|
|
437
|
+
e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
|
|
438
|
+
# Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
|
|
439
|
+
reason = str(type(ex)) + ":<'" + str(ex) + "'>"
|
|
440
|
+
exc_entity = "ClientApp"
|
|
441
|
+
if isinstance(ex, LoadClientAppError):
|
|
442
|
+
reason = (
|
|
443
|
+
"An exception was raised when attempting to load "
|
|
444
|
+
"`ClientApp`"
|
|
445
|
+
)
|
|
446
|
+
e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
|
|
447
|
+
exc_entity = "SuperNode"
|
|
448
|
+
|
|
449
|
+
if not app_state_tracker.interrupt:
|
|
450
|
+
log(
|
|
451
|
+
ERROR, "%s raised an exception", exc_entity, exc_info=ex
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Create error message
|
|
455
|
+
reply_message = message.create_error_reply(
|
|
456
|
+
error=Error(code=e_code, reason=reason)
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
# No exception, update node state
|
|
460
|
+
node_state.update_context(
|
|
461
|
+
run_id=run_id,
|
|
462
|
+
context=context,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Send
|
|
466
|
+
send(reply_message)
|
|
467
|
+
log(INFO, "Sent reply")
|
|
468
|
+
|
|
469
|
+
except StopIteration:
|
|
470
|
+
sleep_duration = 0
|
|
471
|
+
break
|
|
511
472
|
|
|
512
473
|
# Unregister node
|
|
513
|
-
if delete_node is not None:
|
|
474
|
+
if delete_node is not None and app_state_tracker.is_connected:
|
|
514
475
|
delete_node() # pylint: disable=not-callable
|
|
515
476
|
|
|
516
477
|
if sleep_duration == 0:
|
|
517
478
|
log(INFO, "Disconnect and shut down")
|
|
479
|
+
del app_state_tracker
|
|
518
480
|
break
|
|
481
|
+
|
|
519
482
|
# Sleep and reconnect afterwards
|
|
520
483
|
log(
|
|
521
484
|
INFO,
|
|
@@ -628,13 +591,22 @@ def start_numpy_client(
|
|
|
628
591
|
|
|
629
592
|
def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
630
593
|
Callable[
|
|
631
|
-
[
|
|
594
|
+
[
|
|
595
|
+
str,
|
|
596
|
+
bool,
|
|
597
|
+
RetryInvoker,
|
|
598
|
+
int,
|
|
599
|
+
Union[bytes, str, None],
|
|
600
|
+
Optional[Tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
|
|
601
|
+
],
|
|
632
602
|
ContextManager[
|
|
633
603
|
Tuple[
|
|
634
604
|
Callable[[], Optional[Message]],
|
|
635
605
|
Callable[[Message], None],
|
|
606
|
+
Optional[Callable[[], Optional[int]]],
|
|
636
607
|
Optional[Callable[[], None]],
|
|
637
|
-
Optional[Callable[[],
|
|
608
|
+
Optional[Callable[[int], Run]],
|
|
609
|
+
Optional[Callable[[str], Fab]],
|
|
638
610
|
]
|
|
639
611
|
],
|
|
640
612
|
],
|
|
@@ -668,6 +640,8 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
|
668
640
|
connection, error_type = http_request_response, RequestsConnectionError
|
|
669
641
|
elif transport == TRANSPORT_TYPE_GRPC_RERE:
|
|
670
642
|
connection, error_type = grpc_request_response, RpcError
|
|
643
|
+
elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
|
|
644
|
+
connection, error_type = grpc_adapter, RpcError
|
|
671
645
|
elif transport == TRANSPORT_TYPE_GRPC_BIDI:
|
|
672
646
|
connection, error_type = grpc_connection, RpcError
|
|
673
647
|
else:
|
|
@@ -676,3 +650,20 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
|
676
650
|
)
|
|
677
651
|
|
|
678
652
|
return connection, address, error_type
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@dataclass
|
|
656
|
+
class _AppStateTracker:
|
|
657
|
+
interrupt: bool = False
|
|
658
|
+
is_connected: bool = False
|
|
659
|
+
|
|
660
|
+
def register_signal_handler(self) -> None:
|
|
661
|
+
"""Register handlers for exit signals."""
|
|
662
|
+
|
|
663
|
+
def signal_handler(sig, frame): # type: ignore
|
|
664
|
+
# pylint: disable=unused-argument
|
|
665
|
+
self.interrupt = True
|
|
666
|
+
raise StopIteration from None
|
|
667
|
+
|
|
668
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
669
|
+
signal.signal(signal.SIGTERM, signal_handler)
|