flwr 1.12.0__py3-none-any.whl → 1.13.1__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 +2 -0
- flwr/cli/build.py +2 -2
- flwr/cli/config_utils.py +97 -0
- flwr/cli/install.py +0 -16
- flwr/cli/log.py +63 -97
- flwr/cli/ls.py +228 -0
- flwr/cli/new/new.py +23 -13
- flwr/cli/new/templates/app/README.md.tpl +11 -0
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
- flwr/cli/run/run.py +37 -89
- flwr/client/app.py +73 -34
- flwr/client/clientapp/app.py +58 -37
- flwr/client/grpc_rere_client/connection.py +7 -12
- flwr/client/nodestate/__init__.py +25 -0
- flwr/client/nodestate/in_memory_nodestate.py +38 -0
- flwr/client/nodestate/nodestate.py +30 -0
- flwr/client/nodestate/nodestate_factory.py +37 -0
- flwr/client/rest_client/connection.py +4 -14
- flwr/client/{node_state.py → run_info_store.py} +4 -3
- flwr/client/supernode/app.py +34 -58
- flwr/common/args.py +152 -0
- flwr/common/config.py +10 -0
- flwr/common/constant.py +59 -7
- flwr/common/context.py +9 -4
- flwr/common/date.py +21 -3
- flwr/common/grpc.py +4 -1
- flwr/common/logger.py +108 -1
- flwr/common/object_ref.py +47 -16
- flwr/common/serde.py +34 -0
- flwr/common/telemetry.py +0 -6
- flwr/common/typing.py +32 -2
- flwr/proto/exec_pb2.py +23 -17
- flwr/proto/exec_pb2.pyi +58 -22
- flwr/proto/exec_pb2_grpc.py +34 -0
- flwr/proto/exec_pb2_grpc.pyi +13 -0
- flwr/proto/log_pb2.py +29 -0
- flwr/proto/log_pb2.pyi +39 -0
- flwr/proto/log_pb2_grpc.py +4 -0
- flwr/proto/log_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +8 -8
- flwr/proto/message_pb2.pyi +4 -1
- flwr/proto/run_pb2.py +32 -27
- flwr/proto/run_pb2.pyi +44 -1
- flwr/proto/serverappio_pb2.py +52 -0
- flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +54 -0
- flwr/proto/serverappio_pb2_grpc.py +376 -0
- flwr/proto/serverappio_pb2_grpc.pyi +147 -0
- flwr/proto/simulationio_pb2.py +38 -0
- flwr/proto/simulationio_pb2.pyi +65 -0
- flwr/proto/simulationio_pb2_grpc.py +205 -0
- flwr/proto/simulationio_pb2_grpc.pyi +81 -0
- flwr/server/app.py +297 -162
- flwr/server/driver/driver.py +15 -1
- flwr/server/driver/grpc_driver.py +89 -50
- flwr/server/driver/inmemory_driver.py +6 -16
- flwr/server/run_serverapp.py +11 -235
- flwr/server/{superlink/state → serverapp}/__init__.py +3 -9
- flwr/server/serverapp/app.py +234 -0
- flwr/server/strategy/aggregate.py +4 -4
- flwr/server/strategy/fedadam.py +11 -1
- flwr/server/superlink/driver/__init__.py +1 -1
- flwr/server/superlink/driver/{driver_grpc.py → serverappio_grpc.py} +19 -16
- flwr/server/superlink/driver/{driver_servicer.py → serverappio_servicer.py} +125 -39
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +4 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -2
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -2
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -2
- flwr/server/superlink/fleet/message_handler/message_handler.py +7 -7
- flwr/server/superlink/fleet/rest_rere/rest_api.py +10 -9
- flwr/server/superlink/fleet/vce/vce_api.py +23 -23
- flwr/server/superlink/linkstate/__init__.py +28 -0
- flwr/server/superlink/{state/in_memory_state.py → linkstate/in_memory_linkstate.py} +237 -64
- flwr/server/superlink/{state/state.py → linkstate/linkstate.py} +166 -22
- flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +9 -9
- flwr/server/superlink/{state/sqlite_state.py → linkstate/sqlite_linkstate.py} +383 -174
- flwr/server/superlink/linkstate/utils.py +389 -0
- flwr/server/superlink/simulation/__init__.py +15 -0
- flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
- flwr/server/superlink/simulation/simulationio_servicer.py +153 -0
- flwr/simulation/__init__.py +5 -1
- flwr/simulation/app.py +236 -347
- flwr/simulation/legacy_app.py +402 -0
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -2
- flwr/simulation/run_simulation.py +56 -141
- flwr/simulation/simulationio_connection.py +86 -0
- flwr/superexec/app.py +6 -134
- flwr/superexec/deployment.py +70 -69
- flwr/superexec/exec_grpc.py +15 -8
- flwr/superexec/exec_servicer.py +65 -65
- flwr/superexec/executor.py +26 -7
- flwr/superexec/simulation.py +62 -150
- {flwr-1.12.0.dist-info → flwr-1.13.1.dist-info}/METADATA +9 -7
- {flwr-1.12.0.dist-info → flwr-1.13.1.dist-info}/RECORD +105 -85
- {flwr-1.12.0.dist-info → flwr-1.13.1.dist-info}/entry_points.txt +2 -0
- flwr/client/node_state_tests.py +0 -66
- flwr/proto/driver_pb2.py +0 -42
- flwr/proto/driver_pb2_grpc.py +0 -239
- flwr/proto/driver_pb2_grpc.pyi +0 -94
- flwr/server/superlink/state/utils.py +0 -148
- {flwr-1.12.0.dist-info → flwr-1.13.1.dist-info}/LICENSE +0 -0
- {flwr-1.12.0.dist-info → flwr-1.13.1.dist-info}/WHEEL +0 -0
flwr/cli/app.py
CHANGED
|
@@ -20,6 +20,7 @@ from typer.main import get_command
|
|
|
20
20
|
from .build import build
|
|
21
21
|
from .install import install
|
|
22
22
|
from .log import log
|
|
23
|
+
from .ls import ls
|
|
23
24
|
from .new import new
|
|
24
25
|
from .run import run
|
|
25
26
|
|
|
@@ -37,6 +38,7 @@ app.command()(run)
|
|
|
37
38
|
app.command()(build)
|
|
38
39
|
app.command()(install)
|
|
39
40
|
app.command()(log)
|
|
41
|
+
app.command()(ls)
|
|
40
42
|
|
|
41
43
|
typer_click_object = get_command(app)
|
|
42
44
|
|
flwr/cli/build.py
CHANGED
|
@@ -81,8 +81,8 @@ def build(
|
|
|
81
81
|
if not is_valid_project_name(app.name):
|
|
82
82
|
typer.secho(
|
|
83
83
|
f"❌ The project name {app.name} is invalid, "
|
|
84
|
-
"a valid project name must start with a letter
|
|
85
|
-
"and can only contain letters, digits, and
|
|
84
|
+
"a valid project name must start with a letter, "
|
|
85
|
+
"and can only contain letters, digits, and hyphens.",
|
|
86
86
|
fg=typer.colors.RED,
|
|
87
87
|
bold=True,
|
|
88
88
|
)
|
flwr/cli/config_utils.py
CHANGED
|
@@ -20,6 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
from typing import IO, Any, Optional, Union, get_args
|
|
21
21
|
|
|
22
22
|
import tomli
|
|
23
|
+
import typer
|
|
23
24
|
|
|
24
25
|
from flwr.common import object_ref
|
|
25
26
|
from flwr.common.typing import UserConfigValue
|
|
@@ -227,3 +228,99 @@ def load_from_string(toml_content: str) -> Optional[dict[str, Any]]:
|
|
|
227
228
|
return data
|
|
228
229
|
except tomli.TOMLDecodeError:
|
|
229
230
|
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def validate_project_config(
|
|
234
|
+
config: Union[dict[str, Any], None], errors: list[str], warnings: list[str]
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""Validate and return the Flower project configuration."""
|
|
237
|
+
if config is None:
|
|
238
|
+
typer.secho(
|
|
239
|
+
"Project configuration could not be loaded.\n"
|
|
240
|
+
"pyproject.toml is invalid:\n"
|
|
241
|
+
+ "\n".join([f"- {line}" for line in errors]),
|
|
242
|
+
fg=typer.colors.RED,
|
|
243
|
+
bold=True,
|
|
244
|
+
)
|
|
245
|
+
raise typer.Exit(code=1)
|
|
246
|
+
|
|
247
|
+
if warnings:
|
|
248
|
+
typer.secho(
|
|
249
|
+
"Project configuration is missing the following "
|
|
250
|
+
"recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
|
|
251
|
+
fg=typer.colors.RED,
|
|
252
|
+
bold=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
typer.secho("Success", fg=typer.colors.GREEN)
|
|
256
|
+
|
|
257
|
+
return config
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def validate_federation_in_project_config(
|
|
261
|
+
federation: Optional[str], config: dict[str, Any]
|
|
262
|
+
) -> tuple[str, dict[str, Any]]:
|
|
263
|
+
"""Validate the federation name in the Flower project configuration."""
|
|
264
|
+
federation = federation or config["tool"]["flwr"]["federations"].get("default")
|
|
265
|
+
|
|
266
|
+
if federation is None:
|
|
267
|
+
typer.secho(
|
|
268
|
+
"❌ No federation name was provided and the project's `pyproject.toml` "
|
|
269
|
+
"doesn't declare a default federation (with an Exec API address or an "
|
|
270
|
+
"`options.num-supernodes` value).",
|
|
271
|
+
fg=typer.colors.RED,
|
|
272
|
+
bold=True,
|
|
273
|
+
)
|
|
274
|
+
raise typer.Exit(code=1)
|
|
275
|
+
|
|
276
|
+
# Validate the federation exists in the configuration
|
|
277
|
+
federation_config = config["tool"]["flwr"]["federations"].get(federation)
|
|
278
|
+
if federation_config is None:
|
|
279
|
+
available_feds = {
|
|
280
|
+
fed for fed in config["tool"]["flwr"]["federations"] if fed != "default"
|
|
281
|
+
}
|
|
282
|
+
typer.secho(
|
|
283
|
+
f"❌ There is no `{federation}` federation declared in the "
|
|
284
|
+
"`pyproject.toml`.\n The following federations were found:\n\n"
|
|
285
|
+
+ "\n".join(available_feds),
|
|
286
|
+
fg=typer.colors.RED,
|
|
287
|
+
bold=True,
|
|
288
|
+
)
|
|
289
|
+
raise typer.Exit(code=1)
|
|
290
|
+
|
|
291
|
+
return federation, federation_config
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def validate_certificate_in_federation_config(
|
|
295
|
+
app: Path, federation_config: dict[str, Any]
|
|
296
|
+
) -> tuple[bool, Optional[bytes]]:
|
|
297
|
+
"""Validate the certificates in the Flower project configuration."""
|
|
298
|
+
insecure_str = federation_config.get("insecure")
|
|
299
|
+
if root_certificates := federation_config.get("root-certificates"):
|
|
300
|
+
root_certificates_bytes = (app / root_certificates).read_bytes()
|
|
301
|
+
if insecure := bool(insecure_str):
|
|
302
|
+
typer.secho(
|
|
303
|
+
"❌ `root_certificates` were provided but the `insecure` parameter "
|
|
304
|
+
"is set to `True`.",
|
|
305
|
+
fg=typer.colors.RED,
|
|
306
|
+
bold=True,
|
|
307
|
+
)
|
|
308
|
+
raise typer.Exit(code=1)
|
|
309
|
+
else:
|
|
310
|
+
root_certificates_bytes = None
|
|
311
|
+
if insecure_str is None:
|
|
312
|
+
typer.secho(
|
|
313
|
+
"❌ To disable TLS, set `insecure = true` in `pyproject.toml`.",
|
|
314
|
+
fg=typer.colors.RED,
|
|
315
|
+
bold=True,
|
|
316
|
+
)
|
|
317
|
+
raise typer.Exit(code=1)
|
|
318
|
+
if not (insecure := bool(insecure_str)):
|
|
319
|
+
typer.secho(
|
|
320
|
+
"❌ No certificate were given yet `insecure` is set to `False`.",
|
|
321
|
+
fg=typer.colors.RED,
|
|
322
|
+
bold=True,
|
|
323
|
+
)
|
|
324
|
+
raise typer.Exit(code=1)
|
|
325
|
+
|
|
326
|
+
return insecure, root_certificates_bytes
|
flwr/cli/install.py
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import shutil
|
|
19
|
-
import subprocess
|
|
20
19
|
import tempfile
|
|
21
20
|
import zipfile
|
|
22
21
|
from io import BytesIO
|
|
@@ -188,21 +187,6 @@ def validate_and_install(
|
|
|
188
187
|
else:
|
|
189
188
|
shutil.copy2(item, install_dir / item.name)
|
|
190
189
|
|
|
191
|
-
try:
|
|
192
|
-
subprocess.run(
|
|
193
|
-
["pip", "install", "-e", install_dir, "--no-deps"],
|
|
194
|
-
capture_output=True,
|
|
195
|
-
text=True,
|
|
196
|
-
check=True,
|
|
197
|
-
)
|
|
198
|
-
except subprocess.CalledProcessError as e:
|
|
199
|
-
typer.secho(
|
|
200
|
-
f"❌ Failed to `pip install` package(s) from {install_dir}:\n{e.stderr}",
|
|
201
|
-
fg=typer.colors.RED,
|
|
202
|
-
bold=True,
|
|
203
|
-
)
|
|
204
|
-
raise typer.Exit(code=1) from e
|
|
205
|
-
|
|
206
190
|
typer.secho(
|
|
207
191
|
f"🎊 Successfully installed {project_name} to {install_dir}.",
|
|
208
192
|
fg=typer.colors.GREEN,
|
flwr/cli/log.py
CHANGED
|
@@ -14,33 +14,38 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Flower command line interface `log` command."""
|
|
16
16
|
|
|
17
|
-
import sys
|
|
18
17
|
import time
|
|
19
18
|
from logging import DEBUG, ERROR, INFO
|
|
20
19
|
from pathlib import Path
|
|
21
|
-
from typing import Annotated, Optional
|
|
20
|
+
from typing import Annotated, Any, Optional, cast
|
|
22
21
|
|
|
23
22
|
import grpc
|
|
24
23
|
import typer
|
|
25
24
|
|
|
26
|
-
from flwr.cli.config_utils import
|
|
25
|
+
from flwr.cli.config_utils import (
|
|
26
|
+
load_and_validate,
|
|
27
|
+
validate_certificate_in_federation_config,
|
|
28
|
+
validate_federation_in_project_config,
|
|
29
|
+
validate_project_config,
|
|
30
|
+
)
|
|
31
|
+
from flwr.common.constant import CONN_RECONNECT_INTERVAL, CONN_REFRESH_PERIOD
|
|
27
32
|
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
28
33
|
from flwr.common.logger import log as logger
|
|
29
34
|
from flwr.proto.exec_pb2 import StreamLogsRequest # pylint: disable=E0611
|
|
30
35
|
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
31
36
|
|
|
32
|
-
CONN_REFRESH_PERIOD = 60 # Connection refresh period for log streaming (seconds)
|
|
33
|
-
|
|
34
37
|
|
|
35
38
|
def start_stream(
|
|
36
39
|
run_id: int, channel: grpc.Channel, refresh_period: int = CONN_REFRESH_PERIOD
|
|
37
40
|
) -> None:
|
|
38
41
|
"""Start log streaming for a given run ID."""
|
|
42
|
+
stub = ExecStub(channel)
|
|
43
|
+
after_timestamp = 0.0
|
|
39
44
|
try:
|
|
45
|
+
logger(INFO, "Starting logstream for run_id `%s`", run_id)
|
|
40
46
|
while True:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
time.sleep(2)
|
|
47
|
+
after_timestamp = stream_logs(run_id, stub, refresh_period, after_timestamp)
|
|
48
|
+
time.sleep(CONN_RECONNECT_INTERVAL)
|
|
44
49
|
logger(DEBUG, "Reconnecting to logstream")
|
|
45
50
|
except KeyboardInterrupt:
|
|
46
51
|
logger(INFO, "Exiting logstream")
|
|
@@ -54,16 +59,44 @@ def start_stream(
|
|
|
54
59
|
channel.close()
|
|
55
60
|
|
|
56
61
|
|
|
57
|
-
def stream_logs(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
def stream_logs(
|
|
63
|
+
run_id: int, stub: ExecStub, duration: int, after_timestamp: float
|
|
64
|
+
) -> float:
|
|
65
|
+
"""Stream logs from the beginning of a run with connection refresh.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
run_id : int
|
|
70
|
+
The identifier of the run.
|
|
71
|
+
stub : ExecStub
|
|
72
|
+
The gRPC stub to interact with the Exec service.
|
|
73
|
+
duration : int
|
|
74
|
+
The timeout duration for each stream connection in seconds.
|
|
75
|
+
after_timestamp : float
|
|
76
|
+
The timestamp to start streaming logs from.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
float
|
|
81
|
+
The latest timestamp from the streamed logs or the provided `after_timestamp`
|
|
82
|
+
if no logs are returned.
|
|
83
|
+
"""
|
|
84
|
+
req = StreamLogsRequest(run_id=run_id, after_timestamp=after_timestamp)
|
|
85
|
+
|
|
86
|
+
latest_timestamp = 0.0
|
|
87
|
+
res = None
|
|
88
|
+
try:
|
|
89
|
+
for res in stub.StreamLogs(req, timeout=duration):
|
|
90
|
+
print(res.log_output, end="")
|
|
91
|
+
except grpc.RpcError as e:
|
|
92
|
+
# pylint: disable=E1101
|
|
93
|
+
if e.code() != grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
94
|
+
raise e
|
|
95
|
+
finally:
|
|
96
|
+
if res is not None:
|
|
97
|
+
latest_timestamp = cast(float, res.latest_timestamp)
|
|
62
98
|
|
|
63
|
-
|
|
64
|
-
print(res.log_output)
|
|
65
|
-
if time.time() - start_time > duration:
|
|
66
|
-
break
|
|
99
|
+
return max(latest_timestamp, after_timestamp)
|
|
67
100
|
|
|
68
101
|
|
|
69
102
|
def print_logs(run_id: int, channel: grpc.Channel, timeout: int) -> None:
|
|
@@ -124,100 +157,33 @@ def log(
|
|
|
124
157
|
|
|
125
158
|
pyproject_path = app / "pyproject.toml" if app else None
|
|
126
159
|
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"pyproject.toml is invalid:\n"
|
|
132
|
-
+ "\n".join([f"- {line}" for line in errors]),
|
|
133
|
-
fg=typer.colors.RED,
|
|
134
|
-
bold=True,
|
|
135
|
-
)
|
|
136
|
-
sys.exit()
|
|
137
|
-
|
|
138
|
-
if warnings:
|
|
139
|
-
typer.secho(
|
|
140
|
-
"Project configuration is missing the following "
|
|
141
|
-
"recommended properties:\n" + "\n".join([f"- {line}" for line in warnings]),
|
|
142
|
-
fg=typer.colors.RED,
|
|
143
|
-
bold=True,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
typer.secho("Success", fg=typer.colors.GREEN)
|
|
147
|
-
|
|
148
|
-
federation = federation or config["tool"]["flwr"]["federations"].get("default")
|
|
149
|
-
|
|
150
|
-
if federation is None:
|
|
151
|
-
typer.secho(
|
|
152
|
-
"❌ No federation name was provided and the project's `pyproject.toml` "
|
|
153
|
-
"doesn't declare a default federation (with a SuperExec address or an "
|
|
154
|
-
"`options.num-supernodes` value).",
|
|
155
|
-
fg=typer.colors.RED,
|
|
156
|
-
bold=True,
|
|
157
|
-
)
|
|
158
|
-
raise typer.Exit(code=1)
|
|
159
|
-
|
|
160
|
-
# Validate the federation exists in the configuration
|
|
161
|
-
federation_config = config["tool"]["flwr"]["federations"].get(federation)
|
|
162
|
-
if federation_config is None:
|
|
163
|
-
available_feds = {
|
|
164
|
-
fed for fed in config["tool"]["flwr"]["federations"] if fed != "default"
|
|
165
|
-
}
|
|
166
|
-
typer.secho(
|
|
167
|
-
f"❌ There is no `{federation}` federation declared in the "
|
|
168
|
-
"`pyproject.toml`.\n The following federations were found:\n\n"
|
|
169
|
-
+ "\n".join(available_feds),
|
|
170
|
-
fg=typer.colors.RED,
|
|
171
|
-
bold=True,
|
|
172
|
-
)
|
|
173
|
-
raise typer.Exit(code=1)
|
|
160
|
+
config = validate_project_config(config, errors, warnings)
|
|
161
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
162
|
+
federation, config
|
|
163
|
+
)
|
|
174
164
|
|
|
175
165
|
if "address" not in federation_config:
|
|
176
166
|
typer.secho(
|
|
177
|
-
"❌ `flwr log` currently works with
|
|
178
|
-
"
|
|
167
|
+
"❌ `flwr log` currently works with Exec API. Ensure that the correct"
|
|
168
|
+
"Exec API address is provided in the `pyproject.toml`.",
|
|
179
169
|
fg=typer.colors.RED,
|
|
180
170
|
bold=True,
|
|
181
171
|
)
|
|
182
172
|
raise typer.Exit(code=1)
|
|
183
173
|
|
|
184
|
-
|
|
174
|
+
_log_with_exec_api(app, federation_config, run_id, stream)
|
|
185
175
|
|
|
186
176
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
federation_config: dict[str,
|
|
177
|
+
def _log_with_exec_api(
|
|
178
|
+
app: Path,
|
|
179
|
+
federation_config: dict[str, Any],
|
|
190
180
|
run_id: int,
|
|
191
181
|
stream: bool,
|
|
192
182
|
) -> None:
|
|
193
|
-
insecure_str = federation_config.get("insecure")
|
|
194
|
-
if root_certificates := federation_config.get("root-certificates"):
|
|
195
|
-
root_certificates_bytes = Path(root_certificates).read_bytes()
|
|
196
|
-
if insecure := bool(insecure_str):
|
|
197
|
-
typer.secho(
|
|
198
|
-
"❌ `root_certificates` were provided but the `insecure` parameter"
|
|
199
|
-
"is set to `True`.",
|
|
200
|
-
fg=typer.colors.RED,
|
|
201
|
-
bold=True,
|
|
202
|
-
)
|
|
203
|
-
raise typer.Exit(code=1)
|
|
204
|
-
else:
|
|
205
|
-
root_certificates_bytes = None
|
|
206
|
-
if insecure_str is None:
|
|
207
|
-
typer.secho(
|
|
208
|
-
"❌ To disable TLS, set `insecure = true` in `pyproject.toml`.",
|
|
209
|
-
fg=typer.colors.RED,
|
|
210
|
-
bold=True,
|
|
211
|
-
)
|
|
212
|
-
raise typer.Exit(code=1)
|
|
213
|
-
if not (insecure := bool(insecure_str)):
|
|
214
|
-
typer.secho(
|
|
215
|
-
"❌ No certificate were given yet `insecure` is set to `False`.",
|
|
216
|
-
fg=typer.colors.RED,
|
|
217
|
-
bold=True,
|
|
218
|
-
)
|
|
219
|
-
raise typer.Exit(code=1)
|
|
220
183
|
|
|
184
|
+
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
185
|
+
app, federation_config
|
|
186
|
+
)
|
|
221
187
|
channel = create_channel(
|
|
222
188
|
server_address=federation_config["address"],
|
|
223
189
|
insecure=insecure,
|
flwr/cli/ls.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
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 command line interface `ls` command."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from logging import DEBUG
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Annotated, Any, Optional
|
|
22
|
+
|
|
23
|
+
import grpc
|
|
24
|
+
import typer
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
from flwr.cli.config_utils import (
|
|
30
|
+
load_and_validate,
|
|
31
|
+
validate_certificate_in_federation_config,
|
|
32
|
+
validate_federation_in_project_config,
|
|
33
|
+
validate_project_config,
|
|
34
|
+
)
|
|
35
|
+
from flwr.common.constant import FAB_CONFIG_FILE, SubStatus
|
|
36
|
+
from flwr.common.date import format_timedelta, isoformat8601_utc
|
|
37
|
+
from flwr.common.grpc import GRPC_MAX_MESSAGE_LENGTH, create_channel
|
|
38
|
+
from flwr.common.logger import log
|
|
39
|
+
from flwr.common.serde import run_from_proto
|
|
40
|
+
from flwr.common.typing import Run
|
|
41
|
+
from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
|
|
42
|
+
ListRunsRequest,
|
|
43
|
+
ListRunsResponse,
|
|
44
|
+
)
|
|
45
|
+
from flwr.proto.exec_pb2_grpc import ExecStub
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ls(
|
|
49
|
+
app: Annotated[
|
|
50
|
+
Path,
|
|
51
|
+
typer.Argument(help="Path of the Flower project"),
|
|
52
|
+
] = Path("."),
|
|
53
|
+
federation: Annotated[
|
|
54
|
+
Optional[str],
|
|
55
|
+
typer.Argument(help="Name of the federation"),
|
|
56
|
+
] = None,
|
|
57
|
+
runs: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
typer.Option(
|
|
60
|
+
"--runs",
|
|
61
|
+
help="List all runs",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
64
|
+
run_id: Annotated[
|
|
65
|
+
Optional[int],
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--run-id",
|
|
68
|
+
help="Specific run ID to display",
|
|
69
|
+
),
|
|
70
|
+
] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""List runs."""
|
|
73
|
+
# Load and validate federation config
|
|
74
|
+
typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
|
|
75
|
+
|
|
76
|
+
pyproject_path = app / FAB_CONFIG_FILE if app else None
|
|
77
|
+
config, errors, warnings = load_and_validate(path=pyproject_path)
|
|
78
|
+
config = validate_project_config(config, errors, warnings)
|
|
79
|
+
federation, federation_config = validate_federation_in_project_config(
|
|
80
|
+
federation, config
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if "address" not in federation_config:
|
|
84
|
+
typer.secho(
|
|
85
|
+
"❌ `flwr ls` currently works with Exec API. Ensure that the correct"
|
|
86
|
+
"Exec API address is provided in the `pyproject.toml`.",
|
|
87
|
+
fg=typer.colors.RED,
|
|
88
|
+
bold=True,
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if runs and run_id is not None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"The options '--runs' and '--run-id' are mutually exclusive."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
channel = _init_channel(app, federation_config)
|
|
99
|
+
stub = ExecStub(channel)
|
|
100
|
+
|
|
101
|
+
# Display information about a specific run ID
|
|
102
|
+
if run_id is not None:
|
|
103
|
+
typer.echo(f"🔍 Displaying information for run ID {run_id}...")
|
|
104
|
+
_display_one_run(stub, run_id)
|
|
105
|
+
# By default, list all runs
|
|
106
|
+
else:
|
|
107
|
+
typer.echo("📄 Listing all runs...")
|
|
108
|
+
_list_runs(stub)
|
|
109
|
+
|
|
110
|
+
except ValueError as err:
|
|
111
|
+
typer.secho(
|
|
112
|
+
f"❌ {err}",
|
|
113
|
+
fg=typer.colors.RED,
|
|
114
|
+
bold=True,
|
|
115
|
+
)
|
|
116
|
+
raise typer.Exit(code=1) from err
|
|
117
|
+
finally:
|
|
118
|
+
channel.close()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
122
|
+
"""Log channel connectivity."""
|
|
123
|
+
log(DEBUG, channel_connectivity)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _init_channel(app: Path, federation_config: dict[str, Any]) -> grpc.Channel:
|
|
127
|
+
"""Initialize gRPC channel to the Exec API."""
|
|
128
|
+
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
129
|
+
app, federation_config
|
|
130
|
+
)
|
|
131
|
+
channel = create_channel(
|
|
132
|
+
server_address=federation_config["address"],
|
|
133
|
+
insecure=insecure,
|
|
134
|
+
root_certificates=root_certificates_bytes,
|
|
135
|
+
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
|
136
|
+
interceptors=None,
|
|
137
|
+
)
|
|
138
|
+
channel.subscribe(on_channel_state_change)
|
|
139
|
+
return channel
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _format_run_table(run_dict: dict[int, Run], now_isoformat: str) -> Table:
|
|
143
|
+
"""Format run status as a rich Table."""
|
|
144
|
+
table = Table(header_style="bold cyan", show_lines=True)
|
|
145
|
+
|
|
146
|
+
def _format_datetime(dt: Optional[datetime]) -> str:
|
|
147
|
+
return isoformat8601_utc(dt).replace("T", " ") if dt else "N/A"
|
|
148
|
+
|
|
149
|
+
# Add columns
|
|
150
|
+
table.add_column(
|
|
151
|
+
Text("Run ID", justify="center"), style="bright_white", overflow="fold"
|
|
152
|
+
)
|
|
153
|
+
table.add_column(Text("FAB", justify="center"), style="dim white")
|
|
154
|
+
table.add_column(Text("Status", justify="center"))
|
|
155
|
+
table.add_column(Text("Elapsed", justify="center"), style="blue")
|
|
156
|
+
table.add_column(Text("Created At", justify="center"), style="dim white")
|
|
157
|
+
table.add_column(Text("Running At", justify="center"), style="dim white")
|
|
158
|
+
table.add_column(Text("Finished At", justify="center"), style="dim white")
|
|
159
|
+
|
|
160
|
+
# Add rows
|
|
161
|
+
for run in sorted(
|
|
162
|
+
run_dict.values(), key=lambda x: datetime.fromisoformat(x.pending_at)
|
|
163
|
+
):
|
|
164
|
+
# Combine status and sub-status into a single string
|
|
165
|
+
if run.status.sub_status == "":
|
|
166
|
+
status_text = run.status.status
|
|
167
|
+
else:
|
|
168
|
+
status_text = f"{run.status.status}:{run.status.sub_status}"
|
|
169
|
+
|
|
170
|
+
# Style the status based on its value
|
|
171
|
+
sub_status = run.status.sub_status
|
|
172
|
+
if sub_status == SubStatus.COMPLETED:
|
|
173
|
+
status_style = "green"
|
|
174
|
+
elif sub_status == SubStatus.FAILED:
|
|
175
|
+
status_style = "red"
|
|
176
|
+
else:
|
|
177
|
+
status_style = "yellow"
|
|
178
|
+
|
|
179
|
+
# Convert isoformat to datetime
|
|
180
|
+
pending_at = datetime.fromisoformat(run.pending_at) if run.pending_at else None
|
|
181
|
+
running_at = datetime.fromisoformat(run.running_at) if run.running_at else None
|
|
182
|
+
finished_at = (
|
|
183
|
+
datetime.fromisoformat(run.finished_at) if run.finished_at else None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Calculate elapsed time
|
|
187
|
+
elapsed_time = timedelta()
|
|
188
|
+
if running_at:
|
|
189
|
+
if finished_at:
|
|
190
|
+
end_time = finished_at
|
|
191
|
+
else:
|
|
192
|
+
end_time = datetime.fromisoformat(now_isoformat)
|
|
193
|
+
elapsed_time = end_time - running_at
|
|
194
|
+
|
|
195
|
+
table.add_row(
|
|
196
|
+
f"[bold]{run.run_id}[/bold]",
|
|
197
|
+
f"{run.fab_id} (v{run.fab_version})",
|
|
198
|
+
f"[{status_style}]{status_text}[/{status_style}]",
|
|
199
|
+
format_timedelta(elapsed_time),
|
|
200
|
+
_format_datetime(pending_at),
|
|
201
|
+
_format_datetime(running_at),
|
|
202
|
+
_format_datetime(finished_at),
|
|
203
|
+
)
|
|
204
|
+
return table
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _list_runs(
|
|
208
|
+
stub: ExecStub,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""List all runs."""
|
|
211
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest())
|
|
212
|
+
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
213
|
+
|
|
214
|
+
Console().print(_format_run_table(run_dict, res.now))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _display_one_run(
|
|
218
|
+
stub: ExecStub,
|
|
219
|
+
run_id: int,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""Display information about a specific run."""
|
|
222
|
+
res: ListRunsResponse = stub.ListRuns(ListRunsRequest(run_id=run_id))
|
|
223
|
+
if not res.run_dict:
|
|
224
|
+
raise ValueError(f"Run ID {run_id} not found")
|
|
225
|
+
|
|
226
|
+
run_dict = {run_id: run_from_proto(proto) for run_id, proto in res.run_dict.items()}
|
|
227
|
+
|
|
228
|
+
Console().print(_format_run_table(run_dict, res.now))
|
flwr/cli/new/new.py
CHANGED
|
@@ -268,20 +268,30 @@ def new(
|
|
|
268
268
|
context=context,
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
bold=True,
|
|
277
|
-
)
|
|
271
|
+
prompt = typer.style(
|
|
272
|
+
"🎊 Flower App creation successful.\n\n"
|
|
273
|
+
"To run your Flower App, use the following command:\n\n",
|
|
274
|
+
fg=typer.colors.GREEN,
|
|
275
|
+
bold=True,
|
|
278
276
|
)
|
|
279
277
|
|
|
280
278
|
_add = " huggingface-cli login\n" if llm_challenge_str else ""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
prompt += typer.style(
|
|
280
|
+
_add + f" flwr run {package_name}\n\n",
|
|
281
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
282
|
+
bold=True,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
prompt += typer.style(
|
|
286
|
+
"If you haven't installed all dependencies yet, follow these steps:\n\n",
|
|
287
|
+
fg=typer.colors.GREEN,
|
|
288
|
+
bold=True,
|
|
287
289
|
)
|
|
290
|
+
|
|
291
|
+
prompt += typer.style(
|
|
292
|
+
f" cd {package_name}\n" + " pip install -e .\n" + _add + " flwr run .\n",
|
|
293
|
+
fg=typer.colors.BRIGHT_CYAN,
|
|
294
|
+
bold=True,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
print(prompt)
|
|
@@ -14,7 +14,18 @@ In the `$project_name` directory, use `flwr run` to run a local simulation:
|
|
|
14
14
|
flwr run .
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Refer to the [How to Run Simulations](https://flower.ai/docs/framework/how-to-run-simulations.html) guide in the documentation for advice on how to optimize your simulations.
|
|
18
|
+
|
|
17
19
|
## Run with the Deployment Engine
|
|
18
20
|
|
|
19
21
|
> \[!NOTE\]
|
|
20
22
|
> An update to this example will show how to run this Flower application with the Deployment Engine and TLS certificates, or with Docker.
|
|
23
|
+
|
|
24
|
+
## Resources
|
|
25
|
+
|
|
26
|
+
- Flower website: [flower.ai](https://flower.ai/)
|
|
27
|
+
- Check the documentation: [flower.ai/docs](https://flower.ai/docs/)
|
|
28
|
+
- Give Flower a ⭐️ on GitHub: [GitHub](https://github.com/adap/flower)
|
|
29
|
+
- Join the Flower community!
|
|
30
|
+
- [Flower Slack](https://flower.ai/join-slack/)
|
|
31
|
+
- [Flower Discuss](https://discuss.flower.ai/)
|
|
@@ -71,7 +71,7 @@ def load_data(partition_id: int, num_partitions: int, dataset_name: str):
|
|
|
71
71
|
partitioners={"train": partitioner},
|
|
72
72
|
)
|
|
73
73
|
client_trainset = FDS.load_partition(partition_id, "train")
|
|
74
|
-
client_trainset = reformat(client_trainset, llm_task="
|
|
74
|
+
client_trainset = reformat(client_trainset, llm_task="$llm_challenge_str")
|
|
75
75
|
return client_trainset
|
|
76
76
|
|
|
77
77
|
|