flwr-nightly 1.10.0.dev20240710__py3-none-any.whl → 1.10.0.dev20240712__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/config_utils.py +10 -0
- flwr/cli/run/run.py +25 -8
- flwr/client/app.py +49 -17
- flwr/client/grpc_adapter_client/connection.py +1 -1
- flwr/client/grpc_client/connection.py +1 -1
- flwr/client/grpc_rere_client/connection.py +3 -2
- flwr/client/node_state.py +44 -11
- flwr/client/node_state_tests.py +4 -3
- flwr/client/rest_client/connection.py +4 -3
- flwr/client/supernode/app.py +14 -7
- flwr/common/config.py +3 -3
- flwr/common/context.py +13 -2
- flwr/common/logger.py +25 -0
- flwr/server/__init__.py +2 -0
- flwr/server/compat/legacy_context.py +1 -1
- flwr/server/run_serverapp.py +3 -1
- flwr/server/server_app.py +56 -10
- flwr/server/serverapp_components.py +52 -0
- flwr/server/superlink/fleet/vce/backend/backend.py +4 -4
- flwr/server/superlink/fleet/vce/backend/raybackend.py +8 -9
- flwr/server/superlink/fleet/vce/vce_api.py +88 -121
- flwr/server/typing.py +2 -0
- flwr/simulation/ray_transport/ray_actor.py +15 -19
- flwr/simulation/ray_transport/ray_client_proxy.py +3 -1
- flwr/simulation/run_simulation.py +49 -33
- flwr/superexec/app.py +3 -3
- {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/METADATA +2 -2
- {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/RECORD +31 -30
- {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.10.0.dev20240710.dist-info → flwr_nightly-1.10.0.dev20240712.dist-info}/entry_points.txt +0 -0
flwr/cli/config_utils.py
CHANGED
|
@@ -108,6 +108,14 @@ def load(path: Optional[Path] = None) -> Optional[Dict[str, Any]]:
|
|
|
108
108
|
return load_from_string(toml_file.read())
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def _validate_run_config(config_dict: Dict[str, Any], errors: List[str]) -> None:
|
|
112
|
+
for key, value in config_dict.items():
|
|
113
|
+
if isinstance(value, dict):
|
|
114
|
+
_validate_run_config(config_dict[key], errors)
|
|
115
|
+
elif not isinstance(value, str):
|
|
116
|
+
errors.append(f"Config value of key {key} is not of type `str`.")
|
|
117
|
+
|
|
118
|
+
|
|
111
119
|
# pylint: disable=too-many-branches
|
|
112
120
|
def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
|
|
113
121
|
"""Validate pyproject.toml fields."""
|
|
@@ -133,6 +141,8 @@ def validate_fields(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]
|
|
|
133
141
|
else:
|
|
134
142
|
if "publisher" not in config["flower"]:
|
|
135
143
|
errors.append('Property "publisher" missing in [flower]')
|
|
144
|
+
if "config" in config["flower"]:
|
|
145
|
+
_validate_run_config(config["flower"]["config"], errors)
|
|
136
146
|
if "components" not in config["flower"]:
|
|
137
147
|
errors.append("Missing [flower.components] section")
|
|
138
148
|
else:
|
flwr/cli/run/run.py
CHANGED
|
@@ -18,13 +18,14 @@ import sys
|
|
|
18
18
|
from enum import Enum
|
|
19
19
|
from logging import DEBUG
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Optional
|
|
21
|
+
from typing import Dict, Optional
|
|
22
22
|
|
|
23
23
|
import typer
|
|
24
24
|
from typing_extensions import Annotated
|
|
25
25
|
|
|
26
26
|
from flwr.cli import config_utils
|
|
27
27
|
from flwr.cli.build import build
|
|
28
|
+
from flwr.common.config import parse_config_args
|
|
28
29
|
from flwr.common.constant import SUPEREXEC_DEFAULT_ADDRESS
|
|
29
30
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
30
31
|
from flwr.common.logger import log
|
|
@@ -58,15 +59,20 @@ def run(
|
|
|
58
59
|
Optional[Path],
|
|
59
60
|
typer.Option(help="Path of the Flower project to run"),
|
|
60
61
|
] = None,
|
|
62
|
+
config_overrides: Annotated[
|
|
63
|
+
Optional[str],
|
|
64
|
+
typer.Option(
|
|
65
|
+
"--config",
|
|
66
|
+
"-c",
|
|
67
|
+
help="Override configuration key-value pairs",
|
|
68
|
+
),
|
|
69
|
+
] = None,
|
|
61
70
|
) -> None:
|
|
62
71
|
"""Run Flower project."""
|
|
63
|
-
if use_superexec:
|
|
64
|
-
_start_superexec_run(directory)
|
|
65
|
-
return
|
|
66
|
-
|
|
67
72
|
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
pyproject_path = directory / "pyproject.toml" if directory else None
|
|
75
|
+
config, errors, warnings = config_utils.load_and_validate(path=pyproject_path)
|
|
70
76
|
|
|
71
77
|
if config is None:
|
|
72
78
|
typer.secho(
|
|
@@ -88,6 +94,12 @@ def run(
|
|
|
88
94
|
|
|
89
95
|
typer.secho("Success", fg=typer.colors.GREEN)
|
|
90
96
|
|
|
97
|
+
if use_superexec:
|
|
98
|
+
_start_superexec_run(
|
|
99
|
+
parse_config_args(config_overrides, separator=","), directory
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
|
|
91
103
|
server_app_ref = config["flower"]["components"]["serverapp"]
|
|
92
104
|
client_app_ref = config["flower"]["components"]["clientapp"]
|
|
93
105
|
|
|
@@ -115,7 +127,9 @@ def run(
|
|
|
115
127
|
)
|
|
116
128
|
|
|
117
129
|
|
|
118
|
-
def _start_superexec_run(
|
|
130
|
+
def _start_superexec_run(
|
|
131
|
+
override_config: Dict[str, str], directory: Optional[Path]
|
|
132
|
+
) -> None:
|
|
119
133
|
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
120
134
|
"""Log channel connectivity."""
|
|
121
135
|
log(DEBUG, channel_connectivity)
|
|
@@ -132,6 +146,9 @@ def _start_superexec_run(directory: Optional[Path]) -> None:
|
|
|
132
146
|
|
|
133
147
|
fab_path = build(directory)
|
|
134
148
|
|
|
135
|
-
req = StartRunRequest(
|
|
149
|
+
req = StartRunRequest(
|
|
150
|
+
fab_file=Path(fab_path).read_bytes(),
|
|
151
|
+
override_config=override_config,
|
|
152
|
+
)
|
|
136
153
|
res = stub.StartRun(req)
|
|
137
154
|
typer.secho(f"🎊 Successfully started run {res.run_id}", fg=typer.colors.GREEN)
|
flwr/client/app.py
CHANGED
|
@@ -18,7 +18,8 @@ import signal
|
|
|
18
18
|
import sys
|
|
19
19
|
import time
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
from logging import
|
|
21
|
+
from logging import ERROR, INFO, WARN
|
|
22
|
+
from pathlib import Path
|
|
22
23
|
from typing import Callable, ContextManager, Dict, Optional, Tuple, Type, Union
|
|
23
24
|
|
|
24
25
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
@@ -159,6 +160,7 @@ def start_client(
|
|
|
159
160
|
event(EventType.START_CLIENT_ENTER)
|
|
160
161
|
_start_client_internal(
|
|
161
162
|
server_address=server_address,
|
|
163
|
+
node_config={},
|
|
162
164
|
load_client_app_fn=None,
|
|
163
165
|
client_fn=client_fn,
|
|
164
166
|
client=client,
|
|
@@ -180,6 +182,7 @@ def start_client(
|
|
|
180
182
|
def _start_client_internal(
|
|
181
183
|
*,
|
|
182
184
|
server_address: str,
|
|
185
|
+
node_config: Dict[str, str],
|
|
183
186
|
load_client_app_fn: Optional[Callable[[str, str], ClientApp]] = None,
|
|
184
187
|
client_fn: Optional[ClientFnExt] = None,
|
|
185
188
|
client: Optional[Client] = None,
|
|
@@ -192,7 +195,7 @@ def _start_client_internal(
|
|
|
192
195
|
] = None,
|
|
193
196
|
max_retries: Optional[int] = None,
|
|
194
197
|
max_wait_time: Optional[float] = None,
|
|
195
|
-
|
|
198
|
+
flwr_dir: Optional[Path] = None,
|
|
196
199
|
) -> None:
|
|
197
200
|
"""Start a Flower client node which connects to a Flower server.
|
|
198
201
|
|
|
@@ -202,6 +205,8 @@ def _start_client_internal(
|
|
|
202
205
|
The IPv4 or IPv6 address of the server. If the Flower
|
|
203
206
|
server runs on the same machine on port 8080, then `server_address`
|
|
204
207
|
would be `"[::]:8080"`.
|
|
208
|
+
node_config: Dict[str, str]
|
|
209
|
+
The configuration of the node.
|
|
205
210
|
load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
|
|
206
211
|
A function that can be used to load a `ClientApp` instance.
|
|
207
212
|
client_fn : Optional[ClientFnExt]
|
|
@@ -236,9 +241,8 @@ def _start_client_internal(
|
|
|
236
241
|
The maximum duration before the client stops trying to
|
|
237
242
|
connect to the server in case of connection error.
|
|
238
243
|
If set to None, there is no limit to the total time.
|
|
239
|
-
|
|
240
|
-
The
|
|
241
|
-
prototyping purposes.
|
|
244
|
+
flwr_dir: Optional[Path] (default: None)
|
|
245
|
+
The fully resolved path containing installed Flower Apps.
|
|
242
246
|
"""
|
|
243
247
|
if insecure is None:
|
|
244
248
|
insecure = root_certificates is None
|
|
@@ -291,7 +295,7 @@ def _start_client_internal(
|
|
|
291
295
|
log(WARN, "Connection attempt failed, retrying...")
|
|
292
296
|
else:
|
|
293
297
|
log(
|
|
294
|
-
|
|
298
|
+
WARN,
|
|
295
299
|
"Connection attempt failed, retrying in %.2f seconds",
|
|
296
300
|
retry_state.actual_wait,
|
|
297
301
|
)
|
|
@@ -315,8 +319,10 @@ def _start_client_internal(
|
|
|
315
319
|
on_backoff=_on_backoff,
|
|
316
320
|
)
|
|
317
321
|
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
# NodeState gets initialized when the first connection is established
|
|
323
|
+
node_state: Optional[NodeState] = None
|
|
324
|
+
|
|
325
|
+
runs: Dict[int, Run] = {}
|
|
320
326
|
|
|
321
327
|
while not app_state_tracker.interrupt:
|
|
322
328
|
sleep_duration: int = 0
|
|
@@ -330,9 +336,33 @@ def _start_client_internal(
|
|
|
330
336
|
) as conn:
|
|
331
337
|
receive, send, create_node, delete_node, get_run = conn
|
|
332
338
|
|
|
333
|
-
# Register node
|
|
334
|
-
if
|
|
335
|
-
create_node
|
|
339
|
+
# Register node when connecting the first time
|
|
340
|
+
if node_state is None:
|
|
341
|
+
if create_node is None:
|
|
342
|
+
if transport not in ["grpc-bidi", None]:
|
|
343
|
+
raise NotImplementedError(
|
|
344
|
+
"All transports except `grpc-bidi` require "
|
|
345
|
+
"an implementation for `create_node()`.'"
|
|
346
|
+
)
|
|
347
|
+
# gRPC-bidi doesn't have the concept of node_id,
|
|
348
|
+
# so we set it to -1
|
|
349
|
+
node_state = NodeState(
|
|
350
|
+
node_id=-1,
|
|
351
|
+
node_config={},
|
|
352
|
+
partition_id=None,
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
# Call create_node fn to register node
|
|
356
|
+
node_id: Optional[int] = ( # pylint: disable=assignment-from-none
|
|
357
|
+
create_node()
|
|
358
|
+
) # pylint: disable=not-callable
|
|
359
|
+
if node_id is None:
|
|
360
|
+
raise ValueError("Node registration failed")
|
|
361
|
+
node_state = NodeState(
|
|
362
|
+
node_id=node_id,
|
|
363
|
+
node_config=node_config,
|
|
364
|
+
partition_id=None,
|
|
365
|
+
)
|
|
336
366
|
|
|
337
367
|
app_state_tracker.register_signal_handler()
|
|
338
368
|
while not app_state_tracker.interrupt:
|
|
@@ -366,15 +396,17 @@ def _start_client_internal(
|
|
|
366
396
|
|
|
367
397
|
# Get run info
|
|
368
398
|
run_id = message.metadata.run_id
|
|
369
|
-
if run_id not in
|
|
399
|
+
if run_id not in runs:
|
|
370
400
|
if get_run is not None:
|
|
371
|
-
|
|
401
|
+
runs[run_id] = get_run(run_id)
|
|
372
402
|
# If get_run is None, i.e., in grpc-bidi mode
|
|
373
403
|
else:
|
|
374
|
-
|
|
404
|
+
runs[run_id] = Run(run_id, "", "", {})
|
|
375
405
|
|
|
376
406
|
# Register context for this run
|
|
377
|
-
node_state.register_context(
|
|
407
|
+
node_state.register_context(
|
|
408
|
+
run_id=run_id, run=runs[run_id], flwr_dir=flwr_dir
|
|
409
|
+
)
|
|
378
410
|
|
|
379
411
|
# Retrieve context for this run
|
|
380
412
|
context = node_state.retrieve_context(run_id=run_id)
|
|
@@ -388,7 +420,7 @@ def _start_client_internal(
|
|
|
388
420
|
# Handle app loading and task message
|
|
389
421
|
try:
|
|
390
422
|
# Load ClientApp instance
|
|
391
|
-
run: Run =
|
|
423
|
+
run: Run = runs[run_id]
|
|
392
424
|
client_app: ClientApp = load_client_app_fn(
|
|
393
425
|
run.fab_id, run.fab_version
|
|
394
426
|
)
|
|
@@ -574,7 +606,7 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
|
574
606
|
Tuple[
|
|
575
607
|
Callable[[], Optional[Message]],
|
|
576
608
|
Callable[[Message], None],
|
|
577
|
-
Optional[Callable[[],
|
|
609
|
+
Optional[Callable[[], Optional[int]]],
|
|
578
610
|
Optional[Callable[[], None]],
|
|
579
611
|
Optional[Callable[[int], Run]],
|
|
580
612
|
]
|
|
@@ -44,7 +44,7 @@ def grpc_adapter( # pylint: disable=R0913
|
|
|
44
44
|
Tuple[
|
|
45
45
|
Callable[[], Optional[Message]],
|
|
46
46
|
Callable[[Message], None],
|
|
47
|
-
Optional[Callable[[],
|
|
47
|
+
Optional[Callable[[], Optional[int]]],
|
|
48
48
|
Optional[Callable[[], None]],
|
|
49
49
|
Optional[Callable[[int], Run]],
|
|
50
50
|
]
|
|
@@ -72,7 +72,7 @@ def grpc_connection( # pylint: disable=R0913, R0915
|
|
|
72
72
|
Tuple[
|
|
73
73
|
Callable[[], Optional[Message]],
|
|
74
74
|
Callable[[Message], None],
|
|
75
|
-
Optional[Callable[[],
|
|
75
|
+
Optional[Callable[[], Optional[int]]],
|
|
76
76
|
Optional[Callable[[], None]],
|
|
77
77
|
Optional[Callable[[int], Run]],
|
|
78
78
|
]
|
|
@@ -79,7 +79,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
79
79
|
Tuple[
|
|
80
80
|
Callable[[], Optional[Message]],
|
|
81
81
|
Callable[[Message], None],
|
|
82
|
-
Optional[Callable[[],
|
|
82
|
+
Optional[Callable[[], Optional[int]]],
|
|
83
83
|
Optional[Callable[[], None]],
|
|
84
84
|
Optional[Callable[[int], Run]],
|
|
85
85
|
]
|
|
@@ -176,7 +176,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
176
176
|
if not ping_stop_event.is_set():
|
|
177
177
|
ping_stop_event.wait(next_interval)
|
|
178
178
|
|
|
179
|
-
def create_node() ->
|
|
179
|
+
def create_node() -> Optional[int]:
|
|
180
180
|
"""Set create_node."""
|
|
181
181
|
# Call FleetAPI
|
|
182
182
|
create_node_request = CreateNodeRequest(ping_interval=PING_DEFAULT_INTERVAL)
|
|
@@ -189,6 +189,7 @@ def grpc_request_response( # pylint: disable=R0913, R0914, R0915
|
|
|
189
189
|
nonlocal node, ping_thread
|
|
190
190
|
node = cast(Node, create_node_response.node)
|
|
191
191
|
ping_thread = start_ping_loop(ping, ping_stop_event)
|
|
192
|
+
return node.node_id
|
|
192
193
|
|
|
193
194
|
def delete_node() -> None:
|
|
194
195
|
"""Set delete_node."""
|
flwr/client/node_state.py
CHANGED
|
@@ -15,30 +15,58 @@
|
|
|
15
15
|
"""Node state."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
from
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, Optional
|
|
19
21
|
|
|
20
22
|
from flwr.common import Context, RecordSet
|
|
23
|
+
from flwr.common.config import get_fused_config
|
|
24
|
+
from flwr.common.typing import Run
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass()
|
|
28
|
+
class RunInfo:
|
|
29
|
+
"""Contains the Context and initial run_config of a Run."""
|
|
30
|
+
|
|
31
|
+
context: Context
|
|
32
|
+
initial_run_config: Dict[str, str]
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
class NodeState:
|
|
24
36
|
"""State of a node where client nodes execute runs."""
|
|
25
37
|
|
|
26
|
-
def __init__(
|
|
27
|
-
self
|
|
28
|
-
|
|
38
|
+
def __init__(
|
|
39
|
+
self, node_id: int, node_config: Dict[str, str], partition_id: Optional[int]
|
|
40
|
+
) -> None:
|
|
41
|
+
self.node_id = node_id
|
|
42
|
+
self.node_config = node_config
|
|
43
|
+
self.run_infos: Dict[int, RunInfo] = {}
|
|
29
44
|
self._partition_id = partition_id
|
|
30
45
|
|
|
31
|
-
def register_context(
|
|
46
|
+
def register_context(
|
|
47
|
+
self,
|
|
48
|
+
run_id: int,
|
|
49
|
+
run: Optional[Run] = None,
|
|
50
|
+
flwr_dir: Optional[Path] = None,
|
|
51
|
+
) -> None:
|
|
32
52
|
"""Register new run context for this node."""
|
|
33
|
-
if run_id not in self.
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
if run_id not in self.run_infos:
|
|
54
|
+
initial_run_config = get_fused_config(run, flwr_dir) if run else {}
|
|
55
|
+
self.run_infos[run_id] = RunInfo(
|
|
56
|
+
initial_run_config=initial_run_config,
|
|
57
|
+
context=Context(
|
|
58
|
+
node_id=self.node_id,
|
|
59
|
+
node_config=self.node_config,
|
|
60
|
+
state=RecordSet(),
|
|
61
|
+
run_config=initial_run_config.copy(),
|
|
62
|
+
partition_id=self._partition_id,
|
|
63
|
+
),
|
|
36
64
|
)
|
|
37
65
|
|
|
38
66
|
def retrieve_context(self, run_id: int) -> Context:
|
|
39
67
|
"""Get run context given a run_id."""
|
|
40
|
-
if run_id in self.
|
|
41
|
-
return self.
|
|
68
|
+
if run_id in self.run_infos:
|
|
69
|
+
return self.run_infos[run_id].context
|
|
42
70
|
|
|
43
71
|
raise RuntimeError(
|
|
44
72
|
f"Context for run_id={run_id} doesn't exist."
|
|
@@ -48,4 +76,9 @@ class NodeState:
|
|
|
48
76
|
|
|
49
77
|
def update_context(self, run_id: int, context: Context) -> None:
|
|
50
78
|
"""Update run context."""
|
|
51
|
-
self.
|
|
79
|
+
if context.run_config != self.run_infos[run_id].initial_run_config:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"The `run_config` field of the `Context` object cannot be "
|
|
82
|
+
f"modified (run_id: {run_id})."
|
|
83
|
+
)
|
|
84
|
+
self.run_infos[run_id].context = context
|
flwr/client/node_state_tests.py
CHANGED
|
@@ -41,7 +41,7 @@ def test_multirun_in_node_state() -> None:
|
|
|
41
41
|
expected_values = {0: "1", 1: "1" * 3, 2: "1" * 2, 3: "1", 5: "1"}
|
|
42
42
|
|
|
43
43
|
# NodeState
|
|
44
|
-
node_state = NodeState(partition_id=None)
|
|
44
|
+
node_state = NodeState(node_id=0, node_config={}, partition_id=None)
|
|
45
45
|
|
|
46
46
|
for task in tasks:
|
|
47
47
|
run_id = task.run_id
|
|
@@ -59,7 +59,8 @@ def test_multirun_in_node_state() -> None:
|
|
|
59
59
|
node_state.update_context(run_id=run_id, context=updated_state)
|
|
60
60
|
|
|
61
61
|
# Verify values
|
|
62
|
-
for run_id,
|
|
62
|
+
for run_id, run_info in node_state.run_infos.items():
|
|
63
63
|
assert (
|
|
64
|
-
context.state.configs_records["counter"]["count"]
|
|
64
|
+
run_info.context.state.configs_records["counter"]["count"]
|
|
65
|
+
== expected_values[run_id]
|
|
65
66
|
)
|
|
@@ -90,7 +90,7 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
|
|
|
90
90
|
Tuple[
|
|
91
91
|
Callable[[], Optional[Message]],
|
|
92
92
|
Callable[[Message], None],
|
|
93
|
-
Optional[Callable[[],
|
|
93
|
+
Optional[Callable[[], Optional[int]]],
|
|
94
94
|
Optional[Callable[[], None]],
|
|
95
95
|
Optional[Callable[[int], Run]],
|
|
96
96
|
]
|
|
@@ -237,19 +237,20 @@ def http_request_response( # pylint: disable=,R0913, R0914, R0915
|
|
|
237
237
|
if not ping_stop_event.is_set():
|
|
238
238
|
ping_stop_event.wait(next_interval)
|
|
239
239
|
|
|
240
|
-
def create_node() ->
|
|
240
|
+
def create_node() -> Optional[int]:
|
|
241
241
|
"""Set create_node."""
|
|
242
242
|
req = CreateNodeRequest(ping_interval=PING_DEFAULT_INTERVAL)
|
|
243
243
|
|
|
244
244
|
# Send the request
|
|
245
245
|
res = _request(req, CreateNodeResponse, PATH_CREATE_NODE)
|
|
246
246
|
if res is None:
|
|
247
|
-
return
|
|
247
|
+
return None
|
|
248
248
|
|
|
249
249
|
# Remember the node and the ping-loop thread
|
|
250
250
|
nonlocal node, ping_thread
|
|
251
251
|
node = res.node
|
|
252
252
|
ping_thread = start_ping_loop(ping, ping_stop_event)
|
|
253
|
+
return node.node_id
|
|
253
254
|
|
|
254
255
|
def delete_node() -> None:
|
|
255
256
|
"""Set delete_node."""
|
flwr/client/supernode/app.py
CHANGED
|
@@ -29,7 +29,12 @@ from cryptography.hazmat.primitives.serialization import (
|
|
|
29
29
|
|
|
30
30
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
31
31
|
from flwr.common import EventType, event
|
|
32
|
-
from flwr.common.config import
|
|
32
|
+
from flwr.common.config import (
|
|
33
|
+
get_flwr_dir,
|
|
34
|
+
get_project_config,
|
|
35
|
+
get_project_dir,
|
|
36
|
+
parse_config_args,
|
|
37
|
+
)
|
|
33
38
|
from flwr.common.constant import (
|
|
34
39
|
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
35
40
|
TRANSPORT_TYPE_GRPC_RERE,
|
|
@@ -67,7 +72,8 @@ def run_supernode() -> None:
|
|
|
67
72
|
authentication_keys=authentication_keys,
|
|
68
73
|
max_retries=args.max_retries,
|
|
69
74
|
max_wait_time=args.max_wait_time,
|
|
70
|
-
|
|
75
|
+
node_config=parse_config_args(args.node_config),
|
|
76
|
+
flwr_dir=get_flwr_dir(args.flwr_dir),
|
|
71
77
|
)
|
|
72
78
|
|
|
73
79
|
# Graceful shutdown
|
|
@@ -92,6 +98,7 @@ def run_client_app() -> None:
|
|
|
92
98
|
|
|
93
99
|
_start_client_internal(
|
|
94
100
|
server_address=args.superlink,
|
|
101
|
+
node_config=parse_config_args(args.node_config),
|
|
95
102
|
load_client_app_fn=load_fn,
|
|
96
103
|
transport=args.transport,
|
|
97
104
|
root_certificates=root_certificates,
|
|
@@ -388,11 +395,11 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
|
|
|
388
395
|
help="The SuperNode's public key (as a path str) to enable authentication.",
|
|
389
396
|
)
|
|
390
397
|
parser.add_argument(
|
|
391
|
-
"--
|
|
392
|
-
type=
|
|
393
|
-
help="
|
|
394
|
-
"
|
|
395
|
-
"
|
|
398
|
+
"--node-config",
|
|
399
|
+
type=str,
|
|
400
|
+
help="A comma separated list of key/value pairs (separated by `=`) to "
|
|
401
|
+
"configure the SuperNode. "
|
|
402
|
+
"E.g. --node-config 'key1=\"value1\",partition-id=0,num-partitions=100'",
|
|
396
403
|
)
|
|
397
404
|
|
|
398
405
|
|
flwr/common/config.py
CHANGED
|
@@ -121,16 +121,16 @@ def flatten_dict(raw_dict: Dict[str, Any], parent_key: str = "") -> Dict[str, st
|
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def parse_config_args(
|
|
124
|
-
|
|
124
|
+
config: Optional[str],
|
|
125
125
|
separator: str = ",",
|
|
126
126
|
) -> Dict[str, str]:
|
|
127
127
|
"""Parse separator separated list of key-value pairs separated by '='."""
|
|
128
128
|
overrides: Dict[str, str] = {}
|
|
129
129
|
|
|
130
|
-
if
|
|
130
|
+
if config is None:
|
|
131
131
|
return overrides
|
|
132
132
|
|
|
133
|
-
overrides_list =
|
|
133
|
+
overrides_list = config.split(separator)
|
|
134
134
|
if (
|
|
135
135
|
len(overrides_list) == 1
|
|
136
136
|
and "=" not in overrides_list
|
flwr/common/context.py
CHANGED
|
@@ -27,6 +27,11 @@ class Context:
|
|
|
27
27
|
|
|
28
28
|
Parameters
|
|
29
29
|
----------
|
|
30
|
+
node_id : int
|
|
31
|
+
The ID that identifies the node.
|
|
32
|
+
node_config : Dict[str, str]
|
|
33
|
+
A config (key/value mapping) unique to the node and independent of the
|
|
34
|
+
`run_config`. This config persists across all runs this node participates in.
|
|
30
35
|
state : RecordSet
|
|
31
36
|
Holds records added by the entity in a given run and that will stay local.
|
|
32
37
|
This means that the data it holds will never leave the system it's running from.
|
|
@@ -44,16 +49,22 @@ class Context:
|
|
|
44
49
|
simulation or proto typing setups.
|
|
45
50
|
"""
|
|
46
51
|
|
|
52
|
+
node_id: int
|
|
53
|
+
node_config: Dict[str, str]
|
|
47
54
|
state: RecordSet
|
|
48
|
-
partition_id: Optional[int]
|
|
49
55
|
run_config: Dict[str, str]
|
|
56
|
+
partition_id: Optional[int]
|
|
50
57
|
|
|
51
|
-
def __init__(
|
|
58
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
52
59
|
self,
|
|
60
|
+
node_id: int,
|
|
61
|
+
node_config: Dict[str, str],
|
|
53
62
|
state: RecordSet,
|
|
54
63
|
run_config: Dict[str, str],
|
|
55
64
|
partition_id: Optional[int] = None,
|
|
56
65
|
) -> None:
|
|
66
|
+
self.node_id = node_id
|
|
67
|
+
self.node_config = node_config
|
|
57
68
|
self.state = state
|
|
58
69
|
self.run_config = run_config
|
|
59
70
|
self.partition_id = partition_id
|
flwr/common/logger.py
CHANGED
|
@@ -197,6 +197,31 @@ def warn_deprecated_feature(name: str) -> None:
|
|
|
197
197
|
)
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def warn_deprecated_feature_with_example(
|
|
201
|
+
deprecation_message: str, example_message: str, code_example: str
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Warn if a feature is deprecated and show code example."""
|
|
204
|
+
log(
|
|
205
|
+
WARN,
|
|
206
|
+
"""DEPRECATED FEATURE: %s
|
|
207
|
+
|
|
208
|
+
Check the following `FEATURE UPDATE` warning message for the preferred
|
|
209
|
+
new mechanism to use this feature in Flower.
|
|
210
|
+
""",
|
|
211
|
+
deprecation_message,
|
|
212
|
+
)
|
|
213
|
+
log(
|
|
214
|
+
WARN,
|
|
215
|
+
"""FEATURE UPDATE: %s
|
|
216
|
+
------------------------------------------------------------
|
|
217
|
+
%s
|
|
218
|
+
------------------------------------------------------------
|
|
219
|
+
""",
|
|
220
|
+
example_message,
|
|
221
|
+
code_example,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
200
225
|
def warn_unsupported_feature(name: str) -> None:
|
|
201
226
|
"""Warn the user when they use an unsupported feature."""
|
|
202
227
|
log(
|
flwr/server/__init__.py
CHANGED
|
@@ -28,6 +28,7 @@ from .run_serverapp import run_server_app as run_server_app
|
|
|
28
28
|
from .server import Server as Server
|
|
29
29
|
from .server_app import ServerApp as ServerApp
|
|
30
30
|
from .server_config import ServerConfig as ServerConfig
|
|
31
|
+
from .serverapp_components import ServerAppComponents as ServerAppComponents
|
|
31
32
|
|
|
32
33
|
__all__ = [
|
|
33
34
|
"ClientManager",
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"LegacyContext",
|
|
37
38
|
"Server",
|
|
38
39
|
"ServerApp",
|
|
40
|
+
"ServerAppComponents",
|
|
39
41
|
"ServerConfig",
|
|
40
42
|
"SimpleClientManager",
|
|
41
43
|
"run_server_app",
|
flwr/server/run_serverapp.py
CHANGED
|
@@ -78,7 +78,9 @@ def run(
|
|
|
78
78
|
server_app = _load()
|
|
79
79
|
|
|
80
80
|
# Initialize Context
|
|
81
|
-
context = Context(
|
|
81
|
+
context = Context(
|
|
82
|
+
node_id=0, node_config={}, state=RecordSet(), run_config=server_app_run_config
|
|
83
|
+
)
|
|
82
84
|
|
|
83
85
|
# Call ServerApp
|
|
84
86
|
server_app(driver=driver, context=context)
|