flwr 1.21.0__py3-none-any.whl → 1.23.0__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.
- flwr/cli/app.py +17 -1
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +95 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
- flwr/cli/build.py +118 -47
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +34 -23
- flwr/cli/ls.py +13 -9
- flwr/cli/new/new.py +196 -42
- flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
- flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
- flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
- flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
- flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/{pyproject.pytorch_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
- flwr/cli/pull.py +100 -0
- flwr/cli/run/run.py +11 -7
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +260 -0
- flwr/cli/supernode/register.py +185 -0
- flwr/cli/supernode/unregister.py +138 -0
- flwr/cli/utils.py +109 -69
- flwr/client/__init__.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +6 -8
- flwr/client/grpc_rere_client/connection.py +59 -31
- flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
- flwr/client/rest_client/connection.py +82 -37
- flwr/clientapp/__init__.py +1 -2
- flwr/clientapp/mod/__init__.py +4 -1
- flwr/clientapp/mod/centraldp_mods.py +156 -40
- flwr/clientapp/mod/localdp_mod.py +169 -0
- flwr/clientapp/typing.py +22 -0
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +56 -13
- flwr/common/exit/exit_code.py +24 -10
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +10 -1
- flwr/common/record/typeddict.py +12 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/serde.py +4 -2
- flwr/common/typing.py +7 -6
- flwr/compat/client/app.py +1 -1
- flwr/compat/client/grpc_client/connection.py +2 -2
- flwr/proto/control_pb2.py +48 -31
- flwr/proto/control_pb2.pyi +95 -5
- flwr/proto/control_pb2_grpc.py +136 -0
- flwr/proto/control_pb2_grpc.pyi +52 -0
- flwr/proto/fab_pb2.py +11 -7
- flwr/proto/fab_pb2.pyi +21 -1
- flwr/proto/fleet_pb2.py +31 -23
- flwr/proto/fleet_pb2.pyi +63 -23
- flwr/proto/fleet_pb2_grpc.py +98 -28
- flwr/proto/fleet_pb2_grpc.pyi +45 -13
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +48 -0
- flwr/server/app.py +152 -114
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
- flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +18 -5
- flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
- flwr/server/superlink/linkstate/linkstate.py +107 -24
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
- flwr/serverapp/strategy/__init__.py +26 -0
- flwr/serverapp/strategy/bulyan.py +238 -0
- flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
- flwr/serverapp/strategy/fedadagrad.py +0 -3
- flwr/serverapp/strategy/fedadam.py +0 -3
- flwr/serverapp/strategy/fedavg.py +89 -64
- flwr/serverapp/strategy/fedavgm.py +198 -0
- flwr/serverapp/strategy/fedmedian.py +105 -0
- flwr/serverapp/strategy/fedprox.py +174 -0
- flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
- flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
- flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
- flwr/serverapp/strategy/fedyogi.py +0 -3
- flwr/serverapp/strategy/krum.py +112 -0
- flwr/serverapp/strategy/multikrum.py +247 -0
- flwr/serverapp/strategy/qfedavg.py +252 -0
- flwr/serverapp/strategy/strategy_utils.py +48 -0
- flwr/simulation/app.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +28 -32
- flwr/supercore/cli/flower_superexec.py +26 -1
- flwr/supercore/constant.py +41 -0
- flwr/supercore/object_store/in_memory_object_store.py +0 -4
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
- flwr/supercore/superexec/run_superexec.py +16 -2
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/__init__.py +22 -0
- flwr/superlink/artifact_provider/artifact_provider.py +37 -0
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +91 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
- flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
- flwr/superlink/servicer/control/control_grpc.py +16 -11
- flwr/superlink/servicer/control/control_servicer.py +207 -58
- flwr/supernode/cli/flower_supernode.py +19 -26
- flwr/supernode/runtime/run_clientapp.py +2 -2
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +17 -9
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
- flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/serverapp/dp_fixed_clipping.py +0 -352
- flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
- /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import hashlib
|
|
19
19
|
import time
|
|
20
|
-
from collections.abc import Generator
|
|
20
|
+
from collections.abc import Generator, Sequence
|
|
21
21
|
from logging import ERROR, INFO
|
|
22
22
|
from typing import Any, Optional, cast
|
|
23
23
|
|
|
@@ -25,11 +25,16 @@ import grpc
|
|
|
25
25
|
|
|
26
26
|
from flwr.cli.config_utils import get_fab_metadata
|
|
27
27
|
from flwr.common import Context, RecordDict, now
|
|
28
|
-
from flwr.common.auth_plugin import ControlAuthPlugin
|
|
29
28
|
from flwr.common.constant import (
|
|
30
29
|
FAB_MAX_SIZE,
|
|
30
|
+
HEARTBEAT_DEFAULT_INTERVAL,
|
|
31
31
|
LOG_STREAM_INTERVAL,
|
|
32
|
-
|
|
32
|
+
NO_ACCOUNT_AUTH_MESSAGE,
|
|
33
|
+
NO_ARTIFACT_PROVIDER_MESSAGE,
|
|
34
|
+
NODE_NOT_FOUND_MESSAGE,
|
|
35
|
+
PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
|
|
36
|
+
PUBLIC_KEY_NOT_VALID,
|
|
37
|
+
PULL_UNFINISHED_RUN_MESSAGE,
|
|
33
38
|
RUN_ID_NOT_FOUND_MESSAGE,
|
|
34
39
|
Status,
|
|
35
40
|
SubStatus,
|
|
@@ -47,20 +52,32 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
47
52
|
GetAuthTokensResponse,
|
|
48
53
|
GetLoginDetailsRequest,
|
|
49
54
|
GetLoginDetailsResponse,
|
|
55
|
+
ListNodesRequest,
|
|
56
|
+
ListNodesResponse,
|
|
50
57
|
ListRunsRequest,
|
|
51
58
|
ListRunsResponse,
|
|
59
|
+
PullArtifactsRequest,
|
|
60
|
+
PullArtifactsResponse,
|
|
61
|
+
RegisterNodeRequest,
|
|
62
|
+
RegisterNodeResponse,
|
|
52
63
|
StartRunRequest,
|
|
53
64
|
StartRunResponse,
|
|
54
65
|
StopRunRequest,
|
|
55
66
|
StopRunResponse,
|
|
56
67
|
StreamLogsRequest,
|
|
57
68
|
StreamLogsResponse,
|
|
69
|
+
UnregisterNodeRequest,
|
|
70
|
+
UnregisterNodeResponse,
|
|
58
71
|
)
|
|
72
|
+
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
59
73
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
60
74
|
from flwr.supercore.ffs import FfsFactory
|
|
61
75
|
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
76
|
+
from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
|
|
77
|
+
from flwr.superlink.artifact_provider import ArtifactProvider
|
|
78
|
+
from flwr.superlink.auth_plugin import ControlAuthnPlugin
|
|
62
79
|
|
|
63
|
-
from .
|
|
80
|
+
from .control_account_auth_interceptor import shared_account_info
|
|
64
81
|
|
|
65
82
|
|
|
66
83
|
class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
@@ -72,15 +89,17 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
72
89
|
ffs_factory: FfsFactory,
|
|
73
90
|
objectstore_factory: ObjectStoreFactory,
|
|
74
91
|
is_simulation: bool,
|
|
75
|
-
|
|
92
|
+
authn_plugin: ControlAuthnPlugin,
|
|
93
|
+
artifact_provider: Optional[ArtifactProvider] = None,
|
|
76
94
|
) -> None:
|
|
77
95
|
self.linkstate_factory = linkstate_factory
|
|
78
96
|
self.ffs_factory = ffs_factory
|
|
79
97
|
self.objectstore_factory = objectstore_factory
|
|
80
98
|
self.is_simulation = is_simulation
|
|
81
|
-
self.
|
|
99
|
+
self.authn_plugin = authn_plugin
|
|
100
|
+
self.artifact_provider = artifact_provider
|
|
82
101
|
|
|
83
|
-
def StartRun(
|
|
102
|
+
def StartRun( # pylint: disable=too-many-locals
|
|
84
103
|
self, request: StartRunRequest, context: grpc.ServicerContext
|
|
85
104
|
) -> StartRunResponse:
|
|
86
105
|
"""Create run ID."""
|
|
@@ -96,7 +115,8 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
96
115
|
)
|
|
97
116
|
return StartRunResponse()
|
|
98
117
|
|
|
99
|
-
flwr_aid = shared_account_info.get().flwr_aid
|
|
118
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
119
|
+
_check_flwr_aid_exists(flwr_aid, context)
|
|
100
120
|
override_config = user_config_from_proto(request.override_config)
|
|
101
121
|
federation_options = config_record_from_proto(request.federation_options)
|
|
102
122
|
fab_file = request.fab.content
|
|
@@ -109,7 +129,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
109
129
|
)
|
|
110
130
|
|
|
111
131
|
# Create run
|
|
112
|
-
fab = Fab(
|
|
132
|
+
fab = Fab(
|
|
133
|
+
hashlib.sha256(fab_file).hexdigest(),
|
|
134
|
+
fab_file,
|
|
135
|
+
dict(request.fab.verifications),
|
|
136
|
+
)
|
|
113
137
|
fab_hash = ffs.put(fab.content, {})
|
|
114
138
|
if fab_hash != fab.hash_str:
|
|
115
139
|
raise RuntimeError(
|
|
@@ -126,11 +150,20 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
126
150
|
flwr_aid,
|
|
127
151
|
)
|
|
128
152
|
|
|
153
|
+
# Initialize node config
|
|
154
|
+
node_config = {}
|
|
155
|
+
if self.artifact_provider is not None:
|
|
156
|
+
node_config = {
|
|
157
|
+
"output_dir": self.artifact_provider.output_dir,
|
|
158
|
+
"tmp_dir": self.artifact_provider.tmp_dir,
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
# Create an empty context for the Run
|
|
130
162
|
context = Context(
|
|
131
163
|
run_id=run_id,
|
|
132
164
|
node_id=0,
|
|
133
|
-
|
|
165
|
+
# Dict is invariant in mypy
|
|
166
|
+
node_config=node_config, # type: ignore[arg-type]
|
|
134
167
|
state=RecordDict(),
|
|
135
168
|
run_config={},
|
|
136
169
|
)
|
|
@@ -161,12 +194,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
161
194
|
if not run:
|
|
162
195
|
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
163
196
|
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
_check_flwr_aid_in_run(
|
|
168
|
-
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
169
|
-
)
|
|
197
|
+
# Check if `flwr_aid` matches the run's `flwr_aid`
|
|
198
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
199
|
+
_check_flwr_aid_in_run(flwr_aid=flwr_aid, run=cast(Run, run), context=context)
|
|
170
200
|
|
|
171
201
|
after_timestamp = request.after_timestamp + 1e-6
|
|
172
202
|
while context.is_active():
|
|
@@ -202,20 +232,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
202
232
|
|
|
203
233
|
# Build a set of run IDs for `flwr ls --runs`
|
|
204
234
|
if not request.HasField("run_id"):
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
context.abort(
|
|
211
|
-
grpc.StatusCode.PERMISSION_DENIED,
|
|
212
|
-
"️⛔️ User authentication is enabled, but `flwr_aid` is None",
|
|
213
|
-
)
|
|
214
|
-
run_ids = state.get_run_ids(flwr_aid=flwr_aid)
|
|
215
|
-
else:
|
|
216
|
-
# If no `run_id` is specified and no user auth is enabled,
|
|
217
|
-
# return all run IDs
|
|
218
|
-
run_ids = state.get_run_ids(None)
|
|
235
|
+
# If no `run_id` is specified and account auth is enabled,
|
|
236
|
+
# return run IDs for the authenticated account
|
|
237
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
238
|
+
_check_flwr_aid_exists(flwr_aid, context)
|
|
239
|
+
run_ids = state.get_run_ids(flwr_aid=flwr_aid)
|
|
219
240
|
# Build a set of run IDs for `flwr ls --run-id <run_id>`
|
|
220
241
|
else:
|
|
221
242
|
# Retrieve run ID and run
|
|
@@ -225,13 +246,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
225
246
|
# Exit if `run_id` not found
|
|
226
247
|
if not run:
|
|
227
248
|
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
249
|
+
raise grpc.RpcError() # This line is unreachable
|
|
228
250
|
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
_check_flwr_aid_in_run(
|
|
233
|
-
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
234
|
-
)
|
|
251
|
+
# Check if `flwr_aid` matches the run's `flwr_aid`
|
|
252
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
253
|
+
_check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
|
|
235
254
|
|
|
236
255
|
run_ids = {run_id}
|
|
237
256
|
|
|
@@ -253,13 +272,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
253
272
|
# Exit if `run_id` not found
|
|
254
273
|
if not run:
|
|
255
274
|
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
275
|
+
raise grpc.RpcError() # This line is unreachable
|
|
256
276
|
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
_check_flwr_aid_in_run(
|
|
261
|
-
flwr_aid=flwr_aid, run=cast(Run, run), context=context
|
|
262
|
-
)
|
|
277
|
+
# Check if `flwr_aid` matches the run's `flwr_aid`
|
|
278
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
279
|
+
_check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
|
|
263
280
|
|
|
264
281
|
run_status = state.get_run_status({run_id})[run_id]
|
|
265
282
|
if run_status.status == Status.FINISHED:
|
|
@@ -289,22 +306,22 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
289
306
|
) -> GetLoginDetailsResponse:
|
|
290
307
|
"""Start login."""
|
|
291
308
|
log(INFO, "ControlServicer.GetLoginDetails")
|
|
292
|
-
if self.
|
|
309
|
+
if self.authn_plugin is None:
|
|
293
310
|
context.abort(
|
|
294
311
|
grpc.StatusCode.UNIMPLEMENTED,
|
|
295
|
-
|
|
312
|
+
NO_ACCOUNT_AUTH_MESSAGE,
|
|
296
313
|
)
|
|
297
314
|
raise grpc.RpcError() # This line is unreachable
|
|
298
315
|
|
|
299
316
|
# Get login details
|
|
300
|
-
details = self.
|
|
317
|
+
details = self.authn_plugin.get_login_details()
|
|
301
318
|
|
|
302
319
|
# Return empty response if details is None
|
|
303
320
|
if details is None:
|
|
304
321
|
return GetLoginDetailsResponse()
|
|
305
322
|
|
|
306
323
|
return GetLoginDetailsResponse(
|
|
307
|
-
|
|
324
|
+
authn_type=details.authn_type,
|
|
308
325
|
device_code=details.device_code,
|
|
309
326
|
verification_uri_complete=details.verification_uri_complete,
|
|
310
327
|
expires_in=details.expires_in,
|
|
@@ -316,15 +333,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
316
333
|
) -> GetAuthTokensResponse:
|
|
317
334
|
"""Get auth token."""
|
|
318
335
|
log(INFO, "ControlServicer.GetAuthTokens")
|
|
319
|
-
if self.
|
|
336
|
+
if self.authn_plugin is None:
|
|
320
337
|
context.abort(
|
|
321
338
|
grpc.StatusCode.UNIMPLEMENTED,
|
|
322
|
-
|
|
339
|
+
NO_ACCOUNT_AUTH_MESSAGE,
|
|
323
340
|
)
|
|
324
341
|
raise grpc.RpcError() # This line is unreachable
|
|
325
342
|
|
|
326
343
|
# Get auth tokens
|
|
327
|
-
credentials = self.
|
|
344
|
+
credentials = self.authn_plugin.get_auth_tokens(request.device_code)
|
|
328
345
|
|
|
329
346
|
# Return empty response if credentials is None
|
|
330
347
|
if credentials is None:
|
|
@@ -335,6 +352,132 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
335
352
|
refresh_token=credentials.refresh_token,
|
|
336
353
|
)
|
|
337
354
|
|
|
355
|
+
def PullArtifacts(
|
|
356
|
+
self, request: PullArtifactsRequest, context: grpc.ServicerContext
|
|
357
|
+
) -> PullArtifactsResponse:
|
|
358
|
+
"""Pull artifacts for a given run ID."""
|
|
359
|
+
log(INFO, "ControlServicer.PullArtifacts")
|
|
360
|
+
|
|
361
|
+
# Check if artifact provider is configured
|
|
362
|
+
if self.artifact_provider is None:
|
|
363
|
+
context.abort(
|
|
364
|
+
grpc.StatusCode.UNIMPLEMENTED,
|
|
365
|
+
NO_ARTIFACT_PROVIDER_MESSAGE,
|
|
366
|
+
)
|
|
367
|
+
raise grpc.RpcError() # This line is unreachable
|
|
368
|
+
|
|
369
|
+
# Init link state
|
|
370
|
+
state = self.linkstate_factory.state()
|
|
371
|
+
|
|
372
|
+
# Retrieve run ID and run
|
|
373
|
+
run_id = request.run_id
|
|
374
|
+
run = state.get_run(run_id)
|
|
375
|
+
|
|
376
|
+
# Exit if `run_id` not found
|
|
377
|
+
if not run:
|
|
378
|
+
context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
|
|
379
|
+
raise grpc.RpcError() # This line is unreachable
|
|
380
|
+
|
|
381
|
+
# Exit if the run is not finished yet
|
|
382
|
+
if run.status.status != Status.FINISHED:
|
|
383
|
+
context.abort(
|
|
384
|
+
grpc.StatusCode.FAILED_PRECONDITION, PULL_UNFINISHED_RUN_MESSAGE
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Check if `flwr_aid` matches the run's `flwr_aid`
|
|
388
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
389
|
+
_check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
|
|
390
|
+
|
|
391
|
+
# Call artifact provider
|
|
392
|
+
download_url = self.artifact_provider.get_url(run_id)
|
|
393
|
+
return PullArtifactsResponse(url=download_url)
|
|
394
|
+
|
|
395
|
+
def RegisterNode(
|
|
396
|
+
self, request: RegisterNodeRequest, context: grpc.ServicerContext
|
|
397
|
+
) -> RegisterNodeResponse:
|
|
398
|
+
"""Add a SuperNode."""
|
|
399
|
+
log(INFO, "ControlServicer.RegisterNode")
|
|
400
|
+
|
|
401
|
+
# Verify public key
|
|
402
|
+
try:
|
|
403
|
+
# Attempt to deserialize public key
|
|
404
|
+
pub_key = bytes_to_public_key(request.public_key)
|
|
405
|
+
# Check if it's a NIST EC curve public key
|
|
406
|
+
if not uses_nist_ec_curve(pub_key):
|
|
407
|
+
err_msg = "The provided public key is not a NIST EC curve public key."
|
|
408
|
+
log(ERROR, "%s", err_msg)
|
|
409
|
+
raise ValueError(err_msg)
|
|
410
|
+
except (ValueError, AttributeError) as err:
|
|
411
|
+
log(ERROR, "%s", err)
|
|
412
|
+
context.abort(grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_NOT_VALID)
|
|
413
|
+
|
|
414
|
+
# Init link state
|
|
415
|
+
state = self.linkstate_factory.state()
|
|
416
|
+
node_id = 0
|
|
417
|
+
|
|
418
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
419
|
+
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
420
|
+
try:
|
|
421
|
+
node_id = state.create_node(
|
|
422
|
+
owner_aid=flwr_aid,
|
|
423
|
+
public_key=request.public_key,
|
|
424
|
+
heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
except ValueError:
|
|
428
|
+
# Public key already in use
|
|
429
|
+
log(ERROR, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE)
|
|
430
|
+
context.abort(
|
|
431
|
+
grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
|
|
432
|
+
)
|
|
433
|
+
log(INFO, "[ControlServicer.RegisterNode] Created node_id=%s", node_id)
|
|
434
|
+
|
|
435
|
+
return RegisterNodeResponse(node_id=node_id)
|
|
436
|
+
|
|
437
|
+
def UnregisterNode(
|
|
438
|
+
self, request: UnregisterNodeRequest, context: grpc.ServicerContext
|
|
439
|
+
) -> UnregisterNodeResponse:
|
|
440
|
+
"""Remove a SuperNode."""
|
|
441
|
+
log(INFO, "ControlServicer.UnregisterNode")
|
|
442
|
+
|
|
443
|
+
# Init link state
|
|
444
|
+
state = self.linkstate_factory.state()
|
|
445
|
+
|
|
446
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
447
|
+
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
448
|
+
try:
|
|
449
|
+
state.delete_node(owner_aid=flwr_aid, node_id=request.node_id)
|
|
450
|
+
except ValueError:
|
|
451
|
+
log(ERROR, NODE_NOT_FOUND_MESSAGE)
|
|
452
|
+
context.abort(grpc.StatusCode.NOT_FOUND, NODE_NOT_FOUND_MESSAGE)
|
|
453
|
+
|
|
454
|
+
return UnregisterNodeResponse()
|
|
455
|
+
|
|
456
|
+
def ListNodes(
|
|
457
|
+
self, request: ListNodesRequest, context: grpc.ServicerContext
|
|
458
|
+
) -> ListNodesResponse:
|
|
459
|
+
"""List all SuperNodes."""
|
|
460
|
+
log(INFO, "ControlServicer.ListNodes")
|
|
461
|
+
|
|
462
|
+
if self.is_simulation:
|
|
463
|
+
log(ERROR, "ListNodes is not available in simulation mode.")
|
|
464
|
+
context.abort(
|
|
465
|
+
grpc.StatusCode.UNIMPLEMENTED,
|
|
466
|
+
"ListNodes is not available in simulation mode.",
|
|
467
|
+
)
|
|
468
|
+
raise grpc.RpcError() # This line is unreachable
|
|
469
|
+
|
|
470
|
+
nodes_info: Sequence[NodeInfo] = []
|
|
471
|
+
# Init link state
|
|
472
|
+
state = self.linkstate_factory.state()
|
|
473
|
+
|
|
474
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
|
475
|
+
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
476
|
+
# Retrieve all nodes for the account
|
|
477
|
+
nodes_info = state.get_node_info(owner_aids=[flwr_aid])
|
|
478
|
+
|
|
479
|
+
return ListNodesResponse(nodes_info=nodes_info, now=now().isoformat())
|
|
480
|
+
|
|
338
481
|
|
|
339
482
|
def _create_list_runs_response(
|
|
340
483
|
run_ids: set[int], state: LinkState, store: ObjectStore
|
|
@@ -353,29 +496,35 @@ def _create_list_runs_response(
|
|
|
353
496
|
)
|
|
354
497
|
|
|
355
498
|
|
|
356
|
-
def
|
|
357
|
-
flwr_aid: Optional[str],
|
|
358
|
-
) ->
|
|
359
|
-
"""Guard clause to check if `flwr_aid`
|
|
360
|
-
# `flwr_aid` must not be None. Abort if it is None.
|
|
499
|
+
def _check_flwr_aid_exists(
|
|
500
|
+
flwr_aid: Optional[str], context: grpc.ServicerContext
|
|
501
|
+
) -> str:
|
|
502
|
+
"""Guard clause to check if `flwr_aid` exists."""
|
|
361
503
|
if flwr_aid is None:
|
|
362
504
|
context.abort(
|
|
363
505
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
364
|
-
"️⛔️
|
|
506
|
+
"️⛔️ Failed to fetch the account information.",
|
|
365
507
|
)
|
|
508
|
+
raise RuntimeError # This line is unreachable
|
|
509
|
+
return flwr_aid
|
|
510
|
+
|
|
366
511
|
|
|
512
|
+
def _check_flwr_aid_in_run(
|
|
513
|
+
flwr_aid: Optional[str], run: Run, context: grpc.ServicerContext
|
|
514
|
+
) -> None:
|
|
515
|
+
"""Guard clause to check if `flwr_aid` matches the run's `flwr_aid`."""
|
|
516
|
+
_check_flwr_aid_exists(flwr_aid, context)
|
|
367
517
|
# `run.flwr_aid` must not be an empty string. Abort if it is empty.
|
|
368
518
|
run_flwr_aid = run.flwr_aid
|
|
369
519
|
if not run_flwr_aid:
|
|
370
520
|
context.abort(
|
|
371
521
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
372
|
-
"⛔️
|
|
373
|
-
"with a `flwr_aid`.",
|
|
522
|
+
"⛔️ Run is not associated with a `flwr_aid`.",
|
|
374
523
|
)
|
|
375
524
|
|
|
376
525
|
# Exit if `flwr_aid` does not match the run's `flwr_aid`
|
|
377
526
|
if run_flwr_aid != flwr_aid:
|
|
378
527
|
context.abort(
|
|
379
528
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
380
|
-
"⛔️ Run ID does not belong to the
|
|
529
|
+
"⛔️ Run ID does not belong to the account",
|
|
381
530
|
)
|
|
@@ -22,10 +22,7 @@ from typing import Optional
|
|
|
22
22
|
|
|
23
23
|
from cryptography.exceptions import UnsupportedAlgorithm
|
|
24
24
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
25
|
-
from cryptography.hazmat.primitives.serialization import
|
|
26
|
-
load_ssh_private_key,
|
|
27
|
-
load_ssh_public_key,
|
|
28
|
-
)
|
|
25
|
+
from cryptography.hazmat.primitives.serialization import load_ssh_private_key
|
|
29
26
|
|
|
30
27
|
from flwr.common import EventType, event
|
|
31
28
|
from flwr.common.args import try_obtain_root_certificates
|
|
@@ -64,6 +61,13 @@ def flower_supernode() -> None:
|
|
|
64
61
|
root_certificates = try_obtain_root_certificates(args, args.superlink)
|
|
65
62
|
authentication_keys = _try_setup_client_authentication(args)
|
|
66
63
|
|
|
64
|
+
# Warn if authentication keys are provided but transport is not grpc-rere
|
|
65
|
+
if authentication_keys is not None and args.transport != TRANSPORT_TYPE_GRPC_RERE:
|
|
66
|
+
log(
|
|
67
|
+
WARN,
|
|
68
|
+
"SuperNode Authentication is only supported with the grpc-rere transport.",
|
|
69
|
+
)
|
|
70
|
+
|
|
67
71
|
log(DEBUG, "Isolation mode: %s", args.isolation)
|
|
68
72
|
|
|
69
73
|
start_client_internal(
|
|
@@ -188,12 +192,12 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
|
|
|
188
192
|
parser.add_argument(
|
|
189
193
|
"--auth-supernode-private-key",
|
|
190
194
|
type=str,
|
|
191
|
-
help="
|
|
195
|
+
help="Path to the SuperNode's private key to enable authentication.",
|
|
192
196
|
)
|
|
193
197
|
parser.add_argument(
|
|
194
198
|
"--auth-supernode-public-key",
|
|
195
199
|
type=str,
|
|
196
|
-
help="
|
|
200
|
+
help="This argument is deprecated and will be removed in a future release.",
|
|
197
201
|
)
|
|
198
202
|
parser.add_argument(
|
|
199
203
|
"--node-config",
|
|
@@ -207,12 +211,9 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
|
|
|
207
211
|
def _try_setup_client_authentication(
|
|
208
212
|
args: argparse.Namespace,
|
|
209
213
|
) -> Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]:
|
|
210
|
-
if not args.auth_supernode_private_key
|
|
214
|
+
if not args.auth_supernode_private_key:
|
|
211
215
|
return None
|
|
212
216
|
|
|
213
|
-
if not args.auth_supernode_private_key or not args.auth_supernode_public_key:
|
|
214
|
-
flwr_exit(ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED)
|
|
215
|
-
|
|
216
217
|
try:
|
|
217
218
|
ssh_private_key = load_ssh_private_key(
|
|
218
219
|
Path(args.auth_supernode_private_key).read_bytes(),
|
|
@@ -222,23 +223,15 @@ def _try_setup_client_authentication(
|
|
|
222
223
|
raise ValueError()
|
|
223
224
|
except (ValueError, UnsupportedAlgorithm):
|
|
224
225
|
flwr_exit(
|
|
225
|
-
ExitCode.
|
|
226
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID,
|
|
226
227
|
"Unable to parse the private key file.",
|
|
227
228
|
)
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
except (ValueError, UnsupportedAlgorithm):
|
|
236
|
-
flwr_exit(
|
|
237
|
-
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
|
|
238
|
-
"Unable to parse the public key file.",
|
|
230
|
+
if args.auth_supernode_public_key:
|
|
231
|
+
log(
|
|
232
|
+
WARN,
|
|
233
|
+
"The `--auth-supernode-public-key` flag is deprecated and will be "
|
|
234
|
+
"removed in a future release. The public key is now derived from the "
|
|
235
|
+
"private key provided by `--auth-supernode-private-key`.",
|
|
239
236
|
)
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
ssh_private_key,
|
|
243
|
-
ssh_public_key,
|
|
244
|
-
)
|
|
237
|
+
return ssh_private_key, ssh_private_key.public_key()
|
|
@@ -23,8 +23,8 @@ import grpc
|
|
|
23
23
|
|
|
24
24
|
from flwr.app.error import Error
|
|
25
25
|
from flwr.cli.install import install_from_fab
|
|
26
|
-
from flwr.
|
|
27
|
-
from flwr.
|
|
26
|
+
from flwr.clientapp.client_app import ClientApp, LoadClientAppError
|
|
27
|
+
from flwr.clientapp.utils import get_load_client_app_fn
|
|
28
28
|
from flwr.common import Context, Message
|
|
29
29
|
from flwr.common.config import get_flwr_dir
|
|
30
30
|
from flwr.common.constant import ErrorCode
|
|
@@ -151,7 +151,7 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
|
151
151
|
# Retrieve context, run and fab for this run
|
|
152
152
|
context = cast(Context, state.get_context(run_id))
|
|
153
153
|
run = cast(Run, state.get_run(run_id))
|
|
154
|
-
fab = Fab(run.fab_hash, ffs.get(run.fab_hash)[0]) # type: ignore
|
|
154
|
+
fab = Fab(run.fab_hash, ffs.get(run.fab_hash)[0], ffs.get(run.fab_hash)[1]) # type: ignore
|
|
155
155
|
|
|
156
156
|
return PullAppInputsResponse(
|
|
157
157
|
context=context_to_proto(context),
|
|
@@ -54,6 +54,7 @@ from flwr.common.logger import log
|
|
|
54
54
|
from flwr.common.retry_invoker import RetryInvoker, _make_simple_grpc_retry_invoker
|
|
55
55
|
from flwr.common.telemetry import EventType
|
|
56
56
|
from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
|
|
57
|
+
from flwr.common.version import package_version
|
|
57
58
|
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
|
58
59
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
59
60
|
from flwr.supercore.ffs import Ffs, FfsFactory
|
|
@@ -141,6 +142,19 @@ def start_client_internal(
|
|
|
141
142
|
if insecure is None:
|
|
142
143
|
insecure = root_certificates is None
|
|
143
144
|
|
|
145
|
+
# Insecure HTTP is incompatible with authentication
|
|
146
|
+
if insecure and authentication_keys is not None:
|
|
147
|
+
url_v = f"https://flower.ai/docs/framework/v{package_version}/en/"
|
|
148
|
+
page = "how-to-authenticate-supernodes.html"
|
|
149
|
+
flwr_exit(
|
|
150
|
+
ExitCode.SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED,
|
|
151
|
+
"Insecure connection is enabled, but the SuperNode's private key is "
|
|
152
|
+
"provided for authentication. SuperNode authentication requires a "
|
|
153
|
+
"secure TLS connection with the SuperLink. Please enable TLS by "
|
|
154
|
+
"providing the certificate via `--root-certificates`. Please refer "
|
|
155
|
+
f"to the Flower documentation for more information: {url_v}{page}",
|
|
156
|
+
)
|
|
157
|
+
|
|
144
158
|
# Initialize factories
|
|
145
159
|
state_factory = NodeStateFactory()
|
|
146
160
|
ffs_factory = FfsFactory(get_flwr_dir(flwr_path) / "supernode" / "ffs") # type: ignore
|
|
@@ -193,21 +207,16 @@ def start_client_internal(
|
|
|
193
207
|
max_wait_time=max_wait_time,
|
|
194
208
|
) as conn:
|
|
195
209
|
(
|
|
210
|
+
node_id,
|
|
196
211
|
receive,
|
|
197
212
|
send,
|
|
198
|
-
create_node,
|
|
199
|
-
_,
|
|
200
213
|
get_run,
|
|
201
214
|
get_fab,
|
|
202
215
|
pull_object,
|
|
203
216
|
push_object,
|
|
204
217
|
confirm_message_received,
|
|
205
218
|
) = conn
|
|
206
|
-
|
|
207
|
-
# Call create_node fn to register node
|
|
208
|
-
# and store node_id in state
|
|
209
|
-
if (node_id := create_node()) is None:
|
|
210
|
-
raise ValueError("Failed to register SuperNode with the SuperLink")
|
|
219
|
+
# Store node_id in state
|
|
211
220
|
state.set_node_id(node_id)
|
|
212
221
|
|
|
213
222
|
# pylint: disable=too-many-nested-blocks
|
|
@@ -443,10 +452,9 @@ def _init_connection( # pylint: disable=too-many-positional-arguments
|
|
|
443
452
|
max_wait_time: Optional[float] = None,
|
|
444
453
|
) -> Iterator[
|
|
445
454
|
tuple[
|
|
455
|
+
int,
|
|
446
456
|
Callable[[], Optional[tuple[Message, ObjectTree]]],
|
|
447
457
|
Callable[[Message, ObjectTree], set[str]],
|
|
448
|
-
Callable[[], Optional[int]],
|
|
449
|
-
Callable[[], None],
|
|
450
458
|
Callable[[int], Run],
|
|
451
459
|
Callable[[str, int], Fab],
|
|
452
460
|
Callable[[int, str], bytes],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flwr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.23.0
|
|
4
4
|
Summary: Flower: A Friendly Federated AI Framework
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
|
|
@@ -102,25 +102,15 @@ Meet the Flower community on [flower.ai](https://flower.ai)!
|
|
|
102
102
|
|
|
103
103
|
Flower's goal is to make federated learning accessible to everyone. This series of tutorials introduces the fundamentals of federated learning and how to implement them in Flower.
|
|
104
104
|
|
|
105
|
-
0. **What is Federated Learning
|
|
105
|
+
0. **[What is Federated Learning?](https://flower.ai/docs/framework/main/en/tutorial-series-what-is-federated-learning.html)**
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
1. **[An Introduction to Federated Learning](https://flower.ai/docs/framework/main/en/tutorial-series-get-started-with-flower-pytorch.html)**
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
2. **[Using Strategies in Federated Learning](https://flower.ai/docs/framework/main/en/tutorial-series-use-a-federated-learning-strategy-pytorch.html)**
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
3. **[Customize a Flower Strategy](https://flower.ai/docs/framework/main/en/tutorial-series-build-a-strategy-from-scratch-pytorch.html)**
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
[](https://colab.research.google.com/github/adap/flower/blob/main/framework/docs/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb) (or open the [Jupyter Notebook](https://github.com/adap/flower/blob/main/framework/docs/source/tutorial-series-use-a-federated-learning-strategy-pytorch.ipynb))
|
|
116
|
-
|
|
117
|
-
3. **Building Strategies for Federated Learning**
|
|
118
|
-
|
|
119
|
-
[](https://colab.research.google.com/github/adap/flower/blob/main/framework/docs/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb) (or open the [Jupyter Notebook](https://github.com/adap/flower/blob/main/framework/docs/source/tutorial-series-build-a-strategy-from-scratch-pytorch.ipynb))
|
|
120
|
-
|
|
121
|
-
4. **Custom Clients for Federated Learning**
|
|
122
|
-
|
|
123
|
-
[](https://colab.research.google.com/github/adap/flower/blob/main/framework/docs/source/tutorial-series-customize-the-client-pytorch.ipynb) (or open the [Jupyter Notebook](https://github.com/adap/flower/blob/main/framework/docs/source/tutorial-series-customize-the-client-pytorch.ipynb))
|
|
113
|
+
4. **[Communicate Custom Messages](https://flower.ai/docs/framework/main/en/tutorial-series-customize-the-client-pytorch.html)**
|
|
124
114
|
|
|
125
115
|
Stay tuned, more tutorials are coming soon. Topics include **Privacy and Security in Federated Learning**, and **Scaling Federated Learning**.
|
|
126
116
|
|