flwr-nightly 1.9.0.dev20240416__py3-none-any.whl → 1.9.0.dev20240420__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/{flower_toml.py → config_utils.py} +40 -7
- flwr/cli/new/new.py +9 -5
- flwr/cli/new/templates/app/.gitignore.tpl +160 -0
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +56 -0
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +18 -0
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +4 -0
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +4 -0
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +4 -0
- flwr/cli/run/run.py +2 -2
- flwr/client/__init__.py +2 -0
- flwr/client/app.py +7 -53
- flwr/client/grpc_client/connection.py +2 -1
- flwr/client/grpc_rere_client/connection.py +16 -2
- flwr/client/rest_client/connection.py +87 -168
- flwr/client/supernode/__init__.py +22 -0
- flwr/client/supernode/app.py +107 -0
- flwr/common/record/recordset.py +67 -28
- flwr/common/telemetry.py +4 -0
- flwr/server/app.py +5 -5
- flwr/server/compat/app_utils.py +1 -1
- flwr/server/compat/driver_client_proxy.py +4 -2
- flwr/server/driver/__init__.py +0 -2
- flwr/server/driver/abc_driver.py +140 -0
- flwr/server/driver/driver.py +124 -21
- flwr/server/superlink/driver/driver_servicer.py +1 -1
- flwr/server/superlink/fleet/message_handler/message_handler.py +4 -1
- flwr/server/superlink/state/in_memory_state.py +13 -4
- flwr/server/superlink/state/sqlite_state.py +17 -5
- flwr/server/superlink/state/state.py +21 -3
- {flwr_nightly-1.9.0.dev20240416.dist-info → flwr_nightly-1.9.0.dev20240420.dist-info}/METADATA +1 -1
- {flwr_nightly-1.9.0.dev20240416.dist-info → flwr_nightly-1.9.0.dev20240420.dist-info}/RECORD +34 -32
- {flwr_nightly-1.9.0.dev20240416.dist-info → flwr_nightly-1.9.0.dev20240420.dist-info}/entry_points.txt +1 -0
- flwr/cli/new/templates/app/flower.toml.tpl +0 -13
- flwr/server/driver/grpc_driver.py +0 -129
- {flwr_nightly-1.9.0.dev20240416.dist-info → flwr_nightly-1.9.0.dev20240420.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.9.0.dev20240416.dist-info → flwr_nightly-1.9.0.dev20240420.dist-info}/WHEEL +0 -0
|
@@ -21,7 +21,9 @@ import threading
|
|
|
21
21
|
from contextlib import contextmanager
|
|
22
22
|
from copy import copy
|
|
23
23
|
from logging import ERROR, INFO, WARN
|
|
24
|
-
from typing import Callable, Iterator, Optional, Tuple, Union
|
|
24
|
+
from typing import Callable, Iterator, Optional, Tuple, Type, TypeVar, Union
|
|
25
|
+
|
|
26
|
+
from google.protobuf.message import Message as GrpcMessage
|
|
25
27
|
|
|
26
28
|
from flwr.client.heartbeat import start_ping_loop
|
|
27
29
|
from flwr.client.message_handler.message_handler import validate_out_message
|
|
@@ -42,6 +44,9 @@ from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
|
42
44
|
CreateNodeRequest,
|
|
43
45
|
CreateNodeResponse,
|
|
44
46
|
DeleteNodeRequest,
|
|
47
|
+
DeleteNodeResponse,
|
|
48
|
+
GetRunRequest,
|
|
49
|
+
GetRunResponse,
|
|
45
50
|
PingRequest,
|
|
46
51
|
PingResponse,
|
|
47
52
|
PullTaskInsRequest,
|
|
@@ -63,6 +68,9 @@ PATH_DELETE_NODE: str = "api/v0/fleet/delete-node"
|
|
|
63
68
|
PATH_PULL_TASK_INS: str = "api/v0/fleet/pull-task-ins"
|
|
64
69
|
PATH_PUSH_TASK_RES: str = "api/v0/fleet/push-task-res"
|
|
65
70
|
PATH_PING: str = "api/v0/fleet/ping"
|
|
71
|
+
PATH_GET_RUN: str = "/api/v0/fleet/get-run"
|
|
72
|
+
|
|
73
|
+
T = TypeVar("T", bound=GrpcMessage)
|
|
66
74
|
|
|
67
75
|
|
|
68
76
|
@contextmanager
|
|
@@ -80,6 +88,7 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
80
88
|
Callable[[Message], None],
|
|
81
89
|
Optional[Callable[[], None]],
|
|
82
90
|
Optional[Callable[[], None]],
|
|
91
|
+
Optional[Callable[[int], Tuple[str, str]]],
|
|
83
92
|
]
|
|
84
93
|
]:
|
|
85
94
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -141,55 +150,72 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
141
150
|
ping_stop_event = threading.Event()
|
|
142
151
|
|
|
143
152
|
###########################################################################
|
|
144
|
-
# ping/create_node/delete_node/receive/send functions
|
|
153
|
+
# ping/create_node/delete_node/receive/send/get_run functions
|
|
145
154
|
###########################################################################
|
|
146
155
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# Construct the ping request
|
|
154
|
-
req = PingRequest(node=node, ping_interval=PING_DEFAULT_INTERVAL)
|
|
155
|
-
req_bytes: bytes = req.SerializeToString()
|
|
156
|
+
def _request(
|
|
157
|
+
req: GrpcMessage, res_type: Type[T], api_path: str, retry: bool = True
|
|
158
|
+
) -> Optional[T]:
|
|
159
|
+
# Serialize the request
|
|
160
|
+
req_bytes = req.SerializeToString()
|
|
156
161
|
|
|
157
162
|
# Send the request
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
def post() -> requests.Response:
|
|
164
|
+
return requests.post(
|
|
165
|
+
f"{base_url}/{api_path}",
|
|
166
|
+
data=req_bytes,
|
|
167
|
+
headers={
|
|
168
|
+
"Accept": "application/protobuf",
|
|
169
|
+
"Content-Type": "application/protobuf",
|
|
170
|
+
},
|
|
171
|
+
verify=verify,
|
|
172
|
+
timeout=None,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if retry:
|
|
176
|
+
res: requests.Response = retry_invoker.invoke(post)
|
|
177
|
+
else:
|
|
178
|
+
res = post()
|
|
168
179
|
|
|
169
180
|
# Check status code and headers
|
|
170
181
|
if res.status_code != 200:
|
|
171
|
-
return
|
|
182
|
+
return None
|
|
172
183
|
if "content-type" not in res.headers:
|
|
173
184
|
log(
|
|
174
185
|
WARN,
|
|
175
186
|
"[Node] POST /%s: missing header `Content-Type`",
|
|
176
|
-
|
|
187
|
+
api_path,
|
|
177
188
|
)
|
|
178
|
-
return
|
|
189
|
+
return None
|
|
179
190
|
if res.headers["content-type"] != "application/protobuf":
|
|
180
191
|
log(
|
|
181
192
|
WARN,
|
|
182
193
|
"[Node] POST /%s: header `Content-Type` has wrong value",
|
|
183
|
-
|
|
194
|
+
api_path,
|
|
184
195
|
)
|
|
185
|
-
return
|
|
196
|
+
return None
|
|
186
197
|
|
|
187
198
|
# Deserialize ProtoBuf from bytes
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
grpc_res = res_type()
|
|
200
|
+
grpc_res.ParseFromString(res.content)
|
|
201
|
+
return grpc_res
|
|
202
|
+
|
|
203
|
+
def ping() -> None:
|
|
204
|
+
# Get Node
|
|
205
|
+
if node is None:
|
|
206
|
+
log(ERROR, "Node instance missing")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# Construct the ping request
|
|
210
|
+
req = PingRequest(node=node, ping_interval=PING_DEFAULT_INTERVAL)
|
|
211
|
+
|
|
212
|
+
# Send the request
|
|
213
|
+
res = _request(req, PingResponse, PATH_PING, retry=False)
|
|
214
|
+
if res is None:
|
|
215
|
+
return
|
|
190
216
|
|
|
191
217
|
# Check if success
|
|
192
|
-
if not
|
|
218
|
+
if not res.success:
|
|
193
219
|
raise RuntimeError("Ping failed unexpectedly.")
|
|
194
220
|
|
|
195
221
|
# Wait
|
|
@@ -201,46 +227,16 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
201
227
|
|
|
202
228
|
def create_node() -> None:
|
|
203
229
|
"""Set create_node."""
|
|
204
|
-
|
|
205
|
-
create_node_req_bytes: bytes = create_node_req_proto.SerializeToString()
|
|
206
|
-
|
|
207
|
-
res = retry_invoker.invoke(
|
|
208
|
-
requests.post,
|
|
209
|
-
url=f"{base_url}/{PATH_CREATE_NODE}",
|
|
210
|
-
headers={
|
|
211
|
-
"Accept": "application/protobuf",
|
|
212
|
-
"Content-Type": "application/protobuf",
|
|
213
|
-
},
|
|
214
|
-
data=create_node_req_bytes,
|
|
215
|
-
verify=verify,
|
|
216
|
-
timeout=None,
|
|
217
|
-
)
|
|
230
|
+
req = CreateNodeRequest(ping_interval=PING_DEFAULT_INTERVAL)
|
|
218
231
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if "content-type" not in res.headers:
|
|
223
|
-
log(
|
|
224
|
-
WARN,
|
|
225
|
-
"[Node] POST /%s: missing header `Content-Type`",
|
|
226
|
-
PATH_PULL_TASK_INS,
|
|
227
|
-
)
|
|
228
|
-
return
|
|
229
|
-
if res.headers["content-type"] != "application/protobuf":
|
|
230
|
-
log(
|
|
231
|
-
WARN,
|
|
232
|
-
"[Node] POST /%s: header `Content-Type` has wrong value",
|
|
233
|
-
PATH_PULL_TASK_INS,
|
|
234
|
-
)
|
|
232
|
+
# Send the request
|
|
233
|
+
res = _request(req, CreateNodeResponse, PATH_CREATE_NODE)
|
|
234
|
+
if res is None:
|
|
235
235
|
return
|
|
236
236
|
|
|
237
|
-
# Deserialize ProtoBuf from bytes
|
|
238
|
-
create_node_response_proto = CreateNodeResponse()
|
|
239
|
-
create_node_response_proto.ParseFromString(res.content)
|
|
240
|
-
|
|
241
237
|
# Remember the node and the ping-loop thread
|
|
242
238
|
nonlocal node, ping_thread
|
|
243
|
-
node =
|
|
239
|
+
node = res.node
|
|
244
240
|
ping_thread = start_ping_loop(ping, ping_stop_event)
|
|
245
241
|
|
|
246
242
|
def delete_node() -> None:
|
|
@@ -256,36 +252,12 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
256
252
|
ping_thread.join()
|
|
257
253
|
|
|
258
254
|
# Send DeleteNode request
|
|
259
|
-
|
|
260
|
-
delete_node_req_req_bytes: bytes = delete_node_req_proto.SerializeToString()
|
|
261
|
-
res = retry_invoker.invoke(
|
|
262
|
-
requests.post,
|
|
263
|
-
url=f"{base_url}/{PATH_DELETE_NODE}",
|
|
264
|
-
headers={
|
|
265
|
-
"Accept": "application/protobuf",
|
|
266
|
-
"Content-Type": "application/protobuf",
|
|
267
|
-
},
|
|
268
|
-
data=delete_node_req_req_bytes,
|
|
269
|
-
verify=verify,
|
|
270
|
-
timeout=None,
|
|
271
|
-
)
|
|
255
|
+
req = DeleteNodeRequest(node=node)
|
|
272
256
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if "content-type" not in res.headers:
|
|
277
|
-
log(
|
|
278
|
-
WARN,
|
|
279
|
-
"[Node] POST /%s: missing header `Content-Type`",
|
|
280
|
-
PATH_PULL_TASK_INS,
|
|
281
|
-
)
|
|
257
|
+
# Send the request
|
|
258
|
+
res = _request(req, DeleteNodeResponse, PATH_CREATE_NODE)
|
|
259
|
+
if res is None:
|
|
282
260
|
return
|
|
283
|
-
if res.headers["content-type"] != "application/protobuf":
|
|
284
|
-
log(
|
|
285
|
-
WARN,
|
|
286
|
-
"[Node] POST /%s: header `Content-Type` has wrong value",
|
|
287
|
-
PATH_PULL_TASK_INS,
|
|
288
|
-
)
|
|
289
261
|
|
|
290
262
|
# Cleanup
|
|
291
263
|
node = None
|
|
@@ -298,46 +270,15 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
298
270
|
return None
|
|
299
271
|
|
|
300
272
|
# Request instructions (task) from server
|
|
301
|
-
|
|
302
|
-
pull_task_ins_req_bytes: bytes = pull_task_ins_req_proto.SerializeToString()
|
|
273
|
+
req = PullTaskInsRequest(node=node)
|
|
303
274
|
|
|
304
|
-
#
|
|
305
|
-
res =
|
|
306
|
-
|
|
307
|
-
url=f"{base_url}/{PATH_PULL_TASK_INS}",
|
|
308
|
-
headers={
|
|
309
|
-
"Accept": "application/protobuf",
|
|
310
|
-
"Content-Type": "application/protobuf",
|
|
311
|
-
},
|
|
312
|
-
data=pull_task_ins_req_bytes,
|
|
313
|
-
verify=verify,
|
|
314
|
-
timeout=None,
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# Check status code and headers
|
|
318
|
-
if res.status_code != 200:
|
|
319
|
-
return None
|
|
320
|
-
if "content-type" not in res.headers:
|
|
321
|
-
log(
|
|
322
|
-
WARN,
|
|
323
|
-
"[Node] POST /%s: missing header `Content-Type`",
|
|
324
|
-
PATH_PULL_TASK_INS,
|
|
325
|
-
)
|
|
326
|
-
return None
|
|
327
|
-
if res.headers["content-type"] != "application/protobuf":
|
|
328
|
-
log(
|
|
329
|
-
WARN,
|
|
330
|
-
"[Node] POST /%s: header `Content-Type` has wrong value",
|
|
331
|
-
PATH_PULL_TASK_INS,
|
|
332
|
-
)
|
|
275
|
+
# Send the request
|
|
276
|
+
res = _request(req, PullTaskInsResponse, PATH_PULL_TASK_INS)
|
|
277
|
+
if res is None:
|
|
333
278
|
return None
|
|
334
279
|
|
|
335
|
-
# Deserialize ProtoBuf from bytes
|
|
336
|
-
pull_task_ins_response_proto = PullTaskInsResponse()
|
|
337
|
-
pull_task_ins_response_proto.ParseFromString(res.content)
|
|
338
|
-
|
|
339
280
|
# Get the current TaskIns
|
|
340
|
-
task_ins: Optional[TaskIns] = get_task_ins(
|
|
281
|
+
task_ins: Optional[TaskIns] = get_task_ins(res)
|
|
341
282
|
|
|
342
283
|
# Discard the current TaskIns if not valid
|
|
343
284
|
if task_ins is not None and not (
|
|
@@ -372,61 +313,39 @@ def http_request_response( # pylint: disable=R0914, R0915
|
|
|
372
313
|
if not validate_out_message(message, metadata):
|
|
373
314
|
log(ERROR, "Invalid out message")
|
|
374
315
|
return
|
|
316
|
+
metadata = None
|
|
375
317
|
|
|
376
318
|
# Construct TaskRes
|
|
377
319
|
task_res = message_to_taskres(message)
|
|
378
320
|
|
|
379
321
|
# Serialize ProtoBuf to bytes
|
|
380
|
-
|
|
381
|
-
push_task_res_request_bytes: bytes = (
|
|
382
|
-
push_task_res_request_proto.SerializeToString()
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
# Send ClientMessage to server
|
|
386
|
-
res = retry_invoker.invoke(
|
|
387
|
-
requests.post,
|
|
388
|
-
url=f"{base_url}/{PATH_PUSH_TASK_RES}",
|
|
389
|
-
headers={
|
|
390
|
-
"Accept": "application/protobuf",
|
|
391
|
-
"Content-Type": "application/protobuf",
|
|
392
|
-
},
|
|
393
|
-
data=push_task_res_request_bytes,
|
|
394
|
-
verify=verify,
|
|
395
|
-
timeout=None,
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
metadata = None
|
|
322
|
+
req = PushTaskResRequest(task_res_list=[task_res])
|
|
399
323
|
|
|
400
|
-
#
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if "content-type" not in res.headers:
|
|
404
|
-
log(
|
|
405
|
-
WARN,
|
|
406
|
-
"[Node] POST /%s: missing header `Content-Type`",
|
|
407
|
-
PATH_PUSH_TASK_RES,
|
|
408
|
-
)
|
|
409
|
-
return
|
|
410
|
-
if res.headers["content-type"] != "application/protobuf":
|
|
411
|
-
log(
|
|
412
|
-
WARN,
|
|
413
|
-
"[Node] POST /%s: header `Content-Type` has wrong value",
|
|
414
|
-
PATH_PUSH_TASK_RES,
|
|
415
|
-
)
|
|
324
|
+
# Send the request
|
|
325
|
+
res = _request(req, PushTaskResResponse, PATH_PUSH_TASK_RES)
|
|
326
|
+
if res is None:
|
|
416
327
|
return
|
|
417
328
|
|
|
418
|
-
# Deserialize ProtoBuf from bytes
|
|
419
|
-
push_task_res_response_proto = PushTaskResResponse()
|
|
420
|
-
push_task_res_response_proto.ParseFromString(res.content)
|
|
421
329
|
log(
|
|
422
330
|
INFO,
|
|
423
331
|
"[Node] POST /%s: success, created result %s",
|
|
424
332
|
PATH_PUSH_TASK_RES,
|
|
425
|
-
|
|
333
|
+
res.results, # pylint: disable=no-member
|
|
426
334
|
)
|
|
427
335
|
|
|
336
|
+
def get_run(run_id: int) -> Tuple[str, str]:
|
|
337
|
+
# Construct the request
|
|
338
|
+
req = GetRunRequest(run_id=run_id)
|
|
339
|
+
|
|
340
|
+
# Send the request
|
|
341
|
+
res = _request(req, GetRunResponse, PATH_GET_RUN)
|
|
342
|
+
if res is None:
|
|
343
|
+
return "", ""
|
|
344
|
+
|
|
345
|
+
return res.run.fab_id, res.run.fab_version
|
|
346
|
+
|
|
428
347
|
try:
|
|
429
348
|
# Yield methods
|
|
430
|
-
yield (receive, send, create_node, delete_node)
|
|
349
|
+
yield (receive, send, create_node, delete_node, get_run)
|
|
431
350
|
except Exception as exc: # pylint: disable=broad-except
|
|
432
351
|
log(ERROR, exc)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower SuperNode."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .app import run_supernode as run_supernode
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"run_supernode",
|
|
22
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower SuperNode."""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
from logging import DEBUG, INFO
|
|
19
|
+
|
|
20
|
+
from flwr.common import EventType, event
|
|
21
|
+
from flwr.common.exit_handlers import register_exit_handlers
|
|
22
|
+
from flwr.common.logger import log
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_supernode() -> None:
|
|
26
|
+
"""Run Flower SuperNode."""
|
|
27
|
+
log(INFO, "Starting Flower SuperNode")
|
|
28
|
+
|
|
29
|
+
event(EventType.RUN_SUPERNODE_ENTER)
|
|
30
|
+
|
|
31
|
+
args = _parse_args_run_supernode().parse_args()
|
|
32
|
+
|
|
33
|
+
log(
|
|
34
|
+
DEBUG,
|
|
35
|
+
"Flower will load ClientApp `%s`",
|
|
36
|
+
getattr(args, "client-app"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Graceful shutdown
|
|
40
|
+
register_exit_handlers(
|
|
41
|
+
event_type=EventType.RUN_SUPERNODE_LEAVE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_args_run_supernode() -> argparse.ArgumentParser:
|
|
46
|
+
"""Parse flower-supernode command line arguments."""
|
|
47
|
+
parser = argparse.ArgumentParser(
|
|
48
|
+
description="Start a Flower SuperNode",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
parse_args_run_client_app(parser=parser)
|
|
52
|
+
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_args_run_client_app(parser: argparse.ArgumentParser) -> None:
|
|
57
|
+
"""Parse command line arguments."""
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"client-app",
|
|
60
|
+
help="For example: `client:app` or `project.package.module:wrapper.app`",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--insecure",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Run the client without HTTPS. By default, the client runs with "
|
|
66
|
+
"HTTPS enabled. Use this flag only if you understand the risks.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--rest",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Use REST as a transport layer for the client.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--root-certificates",
|
|
75
|
+
metavar="ROOT_CERT",
|
|
76
|
+
type=str,
|
|
77
|
+
help="Specifies the path to the PEM-encoded root certificate file for "
|
|
78
|
+
"establishing secure HTTPS connections.",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--server",
|
|
82
|
+
default="0.0.0.0:9092",
|
|
83
|
+
help="Server address",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--max-retries",
|
|
87
|
+
type=int,
|
|
88
|
+
default=None,
|
|
89
|
+
help="The maximum number of times the client will try to connect to the"
|
|
90
|
+
"server before giving up in case of a connection error. By default,"
|
|
91
|
+
"it is set to None, meaning there is no limit to the number of tries.",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--max-wait-time",
|
|
95
|
+
type=float,
|
|
96
|
+
default=None,
|
|
97
|
+
help="The maximum duration before the client stops trying to"
|
|
98
|
+
"connect to the server in case of connection error. By default, it"
|
|
99
|
+
"is set to None, meaning there is no limit to the total time.",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--dir",
|
|
103
|
+
default="",
|
|
104
|
+
help="Add specified directory to the PYTHONPATH and load Flower "
|
|
105
|
+
"app from there."
|
|
106
|
+
" Default: current working directory.",
|
|
107
|
+
)
|
flwr/common/record/recordset.py
CHANGED
|
@@ -16,23 +16,20 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from dataclasses import dataclass
|
|
19
|
-
from typing import
|
|
19
|
+
from typing import Dict, Optional, cast
|
|
20
20
|
|
|
21
21
|
from .configsrecord import ConfigsRecord
|
|
22
22
|
from .metricsrecord import MetricsRecord
|
|
23
23
|
from .parametersrecord import ParametersRecord
|
|
24
24
|
from .typeddict import TypedDict
|
|
25
25
|
|
|
26
|
-
T = TypeVar("T")
|
|
27
26
|
|
|
27
|
+
class RecordSetData:
|
|
28
|
+
"""Inner data container for the RecordSet class."""
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
_parameters_records: TypedDict[str, ParametersRecord]
|
|
34
|
-
_metrics_records: TypedDict[str, MetricsRecord]
|
|
35
|
-
_configs_records: TypedDict[str, ConfigsRecord]
|
|
30
|
+
parameters_records: TypedDict[str, ParametersRecord]
|
|
31
|
+
metrics_records: TypedDict[str, MetricsRecord]
|
|
32
|
+
configs_records: TypedDict[str, ConfigsRecord]
|
|
36
33
|
|
|
37
34
|
def __init__(
|
|
38
35
|
self,
|
|
@@ -40,40 +37,82 @@ class RecordSet:
|
|
|
40
37
|
metrics_records: Optional[Dict[str, MetricsRecord]] = None,
|
|
41
38
|
configs_records: Optional[Dict[str, ConfigsRecord]] = None,
|
|
42
39
|
) -> None:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if not isinstance(__v, __t):
|
|
46
|
-
raise TypeError(f"Expected `{__t}`, but `{type(__v)}` was passed.")
|
|
47
|
-
|
|
48
|
-
return _check_fn
|
|
49
|
-
|
|
50
|
-
self._parameters_records = TypedDict[str, ParametersRecord](
|
|
51
|
-
_get_check_fn(str), _get_check_fn(ParametersRecord)
|
|
40
|
+
self.parameters_records = TypedDict[str, ParametersRecord](
|
|
41
|
+
self._check_fn_str, self._check_fn_params
|
|
52
42
|
)
|
|
53
|
-
self.
|
|
54
|
-
|
|
43
|
+
self.metrics_records = TypedDict[str, MetricsRecord](
|
|
44
|
+
self._check_fn_str, self._check_fn_metrics
|
|
55
45
|
)
|
|
56
|
-
self.
|
|
57
|
-
|
|
46
|
+
self.configs_records = TypedDict[str, ConfigsRecord](
|
|
47
|
+
self._check_fn_str, self._check_fn_configs
|
|
58
48
|
)
|
|
59
49
|
if parameters_records is not None:
|
|
60
|
-
self.
|
|
50
|
+
self.parameters_records.update(parameters_records)
|
|
61
51
|
if metrics_records is not None:
|
|
62
|
-
self.
|
|
52
|
+
self.metrics_records.update(metrics_records)
|
|
63
53
|
if configs_records is not None:
|
|
64
|
-
self.
|
|
54
|
+
self.configs_records.update(configs_records)
|
|
55
|
+
|
|
56
|
+
def _check_fn_str(self, key: str) -> None:
|
|
57
|
+
if not isinstance(key, str):
|
|
58
|
+
raise TypeError(
|
|
59
|
+
f"Expected `{str.__name__}`, but "
|
|
60
|
+
f"received `{type(key).__name__}` for the key."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _check_fn_params(self, record: ParametersRecord) -> None:
|
|
64
|
+
if not isinstance(record, ParametersRecord):
|
|
65
|
+
raise TypeError(
|
|
66
|
+
f"Expected `{ParametersRecord.__name__}`, but "
|
|
67
|
+
f"received `{type(record).__name__}` for the value."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _check_fn_metrics(self, record: MetricsRecord) -> None:
|
|
71
|
+
if not isinstance(record, MetricsRecord):
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"Expected `{MetricsRecord.__name__}`, but "
|
|
74
|
+
f"received `{type(record).__name__}` for the value."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _check_fn_configs(self, record: ConfigsRecord) -> None:
|
|
78
|
+
if not isinstance(record, ConfigsRecord):
|
|
79
|
+
raise TypeError(
|
|
80
|
+
f"Expected `{ConfigsRecord.__name__}`, but "
|
|
81
|
+
f"received `{type(record).__name__}` for the value."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class RecordSet:
|
|
87
|
+
"""RecordSet stores groups of parameters, metrics and configs."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
parameters_records: Optional[Dict[str, ParametersRecord]] = None,
|
|
92
|
+
metrics_records: Optional[Dict[str, MetricsRecord]] = None,
|
|
93
|
+
configs_records: Optional[Dict[str, ConfigsRecord]] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
data = RecordSetData(
|
|
96
|
+
parameters_records=parameters_records,
|
|
97
|
+
metrics_records=metrics_records,
|
|
98
|
+
configs_records=configs_records,
|
|
99
|
+
)
|
|
100
|
+
setattr(self, "_data", data) # noqa
|
|
65
101
|
|
|
66
102
|
@property
|
|
67
103
|
def parameters_records(self) -> TypedDict[str, ParametersRecord]:
|
|
68
104
|
"""Dictionary holding ParametersRecord instances."""
|
|
69
|
-
|
|
105
|
+
data = cast(RecordSetData, getattr(self, "_data")) # noqa
|
|
106
|
+
return data.parameters_records
|
|
70
107
|
|
|
71
108
|
@property
|
|
72
109
|
def metrics_records(self) -> TypedDict[str, MetricsRecord]:
|
|
73
110
|
"""Dictionary holding MetricsRecord instances."""
|
|
74
|
-
|
|
111
|
+
data = cast(RecordSetData, getattr(self, "_data")) # noqa
|
|
112
|
+
return data.metrics_records
|
|
75
113
|
|
|
76
114
|
@property
|
|
77
115
|
def configs_records(self) -> TypedDict[str, ConfigsRecord]:
|
|
78
116
|
"""Dictionary holding ConfigsRecord instances."""
|
|
79
|
-
|
|
117
|
+
data = cast(RecordSetData, getattr(self, "_data")) # noqa
|
|
118
|
+
return data.configs_records
|
flwr/common/telemetry.py
CHANGED
|
@@ -160,6 +160,10 @@ class EventType(str, Enum):
|
|
|
160
160
|
RUN_SERVER_APP_ENTER = auto()
|
|
161
161
|
RUN_SERVER_APP_LEAVE = auto()
|
|
162
162
|
|
|
163
|
+
# SuperNode
|
|
164
|
+
RUN_SUPERNODE_ENTER = auto()
|
|
165
|
+
RUN_SUPERNODE_LEAVE = auto()
|
|
166
|
+
|
|
163
167
|
|
|
164
168
|
# Use the ThreadPoolExecutor with max_workers=1 to have a queue
|
|
165
169
|
# and also ensure that telemetry calls are not blocking.
|
flwr/server/app.py
CHANGED
|
@@ -291,9 +291,11 @@ def run_fleet_api() -> None:
|
|
|
291
291
|
|
|
292
292
|
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
|
293
293
|
def run_superlink() -> None:
|
|
294
|
-
"""Run Flower
|
|
295
|
-
log(INFO, "Starting Flower
|
|
294
|
+
"""Run Flower SuperLink (Driver API and Fleet API)."""
|
|
295
|
+
log(INFO, "Starting Flower SuperLink")
|
|
296
|
+
|
|
296
297
|
event(EventType.RUN_SUPERLINK_ENTER)
|
|
298
|
+
|
|
297
299
|
args = _parse_args_run_superlink().parse_args()
|
|
298
300
|
|
|
299
301
|
# Parse IP address
|
|
@@ -568,9 +570,7 @@ def _parse_args_run_fleet_api() -> argparse.ArgumentParser:
|
|
|
568
570
|
def _parse_args_run_superlink() -> argparse.ArgumentParser:
|
|
569
571
|
"""Parse command line arguments for both Driver API and Fleet API."""
|
|
570
572
|
parser = argparse.ArgumentParser(
|
|
571
|
-
description="
|
|
572
|
-
"(meaning, a Driver API and a Fleet API), "
|
|
573
|
-
"that clients will be able to connect to.",
|
|
573
|
+
description="Start a Flower SuperLink",
|
|
574
574
|
)
|
|
575
575
|
|
|
576
576
|
_add_args_common(parser=parser)
|
flwr/server/compat/app_utils.py
CHANGED
|
@@ -89,7 +89,7 @@ def _update_client_manager(
|
|
|
89
89
|
for node_id in new_nodes:
|
|
90
90
|
client_proxy = DriverClientProxy(
|
|
91
91
|
node_id=node_id,
|
|
92
|
-
driver=driver.
|
|
92
|
+
driver=driver.grpc_driver_helper, # type: ignore
|
|
93
93
|
anonymous=False,
|
|
94
94
|
run_id=driver.run_id, # type: ignore
|
|
95
95
|
)
|