flwr 1.20.0__py3-none-any.whl → 1.21.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/__init__.py +4 -1
- flwr/app/__init__.py +28 -0
- flwr/app/exception.py +31 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +4 -4
- flwr/cli/cli_user_auth_interceptor.py +1 -1
- flwr/cli/config_utils.py +3 -3
- flwr/cli/constant.py +25 -8
- flwr/cli/log.py +9 -9
- flwr/cli/login/login.py +3 -3
- flwr/cli/ls.py +5 -5
- flwr/cli/new/new.py +11 -0
- flwr/cli/new/templates/app/code/__init__.pytorch_msg_api.py.tpl +1 -0
- flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +80 -0
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +41 -0
- flwr/cli/new/templates/app/code/task.pytorch_msg_api.py.tpl +98 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -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.pytorch_msg_api.toml.tpl +53 -0
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +9 -13
- flwr/cli/stop.py +7 -4
- flwr/cli/utils.py +19 -8
- flwr/client/grpc_rere_client/connection.py +1 -12
- flwr/client/rest_client/connection.py +3 -0
- flwr/clientapp/__init__.py +10 -0
- flwr/clientapp/mod/__init__.py +26 -0
- flwr/clientapp/mod/centraldp_mods.py +132 -0
- flwr/common/args.py +20 -6
- flwr/common/auth_plugin/__init__.py +4 -4
- flwr/common/auth_plugin/auth_plugin.py +7 -7
- flwr/common/constant.py +23 -4
- flwr/common/event_log_plugin/event_log_plugin.py +1 -1
- flwr/common/exit/__init__.py +4 -0
- flwr/common/exit/exit.py +8 -1
- flwr/common/exit/exit_code.py +26 -7
- flwr/common/exit/exit_handler.py +62 -0
- flwr/common/{exit_handlers.py → exit/signal_handler.py} +20 -37
- flwr/common/grpc.py +0 -11
- flwr/common/inflatable_utils.py +1 -1
- flwr/common/logger.py +1 -1
- flwr/common/retry_invoker.py +30 -11
- flwr/common/telemetry.py +4 -0
- flwr/compat/server/app.py +2 -2
- flwr/proto/appio_pb2.py +25 -17
- flwr/proto/appio_pb2.pyi +46 -2
- flwr/proto/clientappio_pb2.py +3 -11
- flwr/proto/clientappio_pb2.pyi +0 -47
- flwr/proto/clientappio_pb2_grpc.py +19 -20
- flwr/proto/clientappio_pb2_grpc.pyi +10 -11
- flwr/proto/control_pb2.py +62 -0
- flwr/proto/{exec_pb2_grpc.py → control_pb2_grpc.py} +54 -54
- flwr/proto/{exec_pb2_grpc.pyi → control_pb2_grpc.pyi} +28 -28
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +68 -0
- flwr/proto/serverappio_pb2_grpc.pyi +26 -0
- flwr/proto/simulationio_pb2.py +4 -11
- flwr/proto/simulationio_pb2.pyi +0 -58
- flwr/proto/simulationio_pb2_grpc.py +129 -27
- flwr/proto/simulationio_pb2_grpc.pyi +52 -13
- flwr/server/app.py +129 -152
- flwr/server/grid/grpc_grid.py +3 -0
- flwr/server/grid/inmemory_grid.py +1 -0
- flwr/server/serverapp/app.py +157 -146
- flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -1
- flwr/server/superlink/fleet/vce/vce_api.py +6 -6
- flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
- flwr/server/superlink/linkstate/linkstate.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +45 -0
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
- flwr/server/superlink/serverappio/serverappio_servicer.py +61 -6
- flwr/server/superlink/simulation/simulationio_servicer.py +97 -21
- flwr/serverapp/__init__.py +12 -0
- flwr/serverapp/dp_fixed_clipping.py +352 -0
- flwr/serverapp/exception.py +38 -0
- flwr/serverapp/strategy/__init__.py +38 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +352 -0
- flwr/serverapp/strategy/fedadagrad.py +162 -0
- flwr/serverapp/strategy/fedadam.py +181 -0
- flwr/serverapp/strategy/fedavg.py +295 -0
- flwr/serverapp/strategy/fedopt.py +218 -0
- flwr/serverapp/strategy/fedyogi.py +173 -0
- flwr/serverapp/strategy/result.py +105 -0
- flwr/serverapp/strategy/strategy.py +285 -0
- flwr/serverapp/strategy/strategy_utils.py +251 -0
- flwr/serverapp/strategy/strategy_utils_tests.py +304 -0
- flwr/simulation/app.py +161 -164
- flwr/supercore/app_utils.py +58 -0
- flwr/{supernode/scheduler → supercore/cli}/__init__.py +3 -3
- flwr/supercore/cli/flower_superexec.py +141 -0
- flwr/supercore/{scheduler → corestate}/__init__.py +3 -3
- flwr/supercore/corestate/corestate.py +81 -0
- flwr/supercore/grpc_health/__init__.py +3 -0
- flwr/supercore/grpc_health/health_server.py +53 -0
- flwr/supercore/grpc_health/simple_health_servicer.py +2 -2
- flwr/{superexec → supercore/superexec}/__init__.py +1 -1
- flwr/supercore/superexec/plugin/__init__.py +28 -0
- flwr/{supernode/scheduler/simple_clientapp_scheduler_plugin.py → supercore/superexec/plugin/base_exec_plugin.py} +10 -6
- flwr/supercore/superexec/plugin/clientapp_exec_plugin.py +28 -0
- flwr/supercore/{scheduler/plugin.py → superexec/plugin/exec_plugin.py} +4 -4
- flwr/supercore/superexec/plugin/serverapp_exec_plugin.py +28 -0
- flwr/supercore/superexec/plugin/simulation_exec_plugin.py +28 -0
- flwr/supercore/superexec/run_superexec.py +185 -0
- flwr/superlink/servicer/__init__.py +15 -0
- flwr/superlink/servicer/control/__init__.py +22 -0
- flwr/{superexec/exec_event_log_interceptor.py → superlink/servicer/control/control_event_log_interceptor.py} +7 -7
- flwr/{superexec/exec_grpc.py → superlink/servicer/control/control_grpc.py} +24 -29
- flwr/{superexec/exec_license_interceptor.py → superlink/servicer/control/control_license_interceptor.py} +6 -6
- flwr/{superexec/exec_servicer.py → superlink/servicer/control/control_servicer.py} +69 -30
- flwr/{superexec/exec_user_auth_interceptor.py → superlink/servicer/control/control_user_auth_interceptor.py} +10 -10
- flwr/supernode/cli/flower_supernode.py +3 -0
- flwr/supernode/cli/flwr_clientapp.py +18 -21
- flwr/supernode/nodestate/in_memory_nodestate.py +2 -2
- flwr/supernode/nodestate/nodestate.py +3 -59
- flwr/supernode/runtime/run_clientapp.py +39 -102
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +10 -17
- flwr/supernode/start_client_internal.py +35 -76
- {flwr-1.20.0.dist-info → flwr-1.21.0.dist-info}/METADATA +4 -3
- {flwr-1.20.0.dist-info → flwr-1.21.0.dist-info}/RECORD +127 -98
- {flwr-1.20.0.dist-info → flwr-1.21.0.dist-info}/entry_points.txt +1 -0
- flwr/proto/exec_pb2.py +0 -62
- flwr/superexec/app.py +0 -45
- flwr/superexec/deployment.py +0 -191
- flwr/superexec/executor.py +0 -100
- flwr/superexec/simulation.py +0 -129
- /flwr/proto/{exec_pb2.pyi → control_pb2.pyi} +0 -0
- {flwr-1.20.0.dist-info → flwr-1.21.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# =====================================================================
|
|
2
|
+
# For a full TOML configuration guide, check the Flower docs:
|
|
3
|
+
# https://flower.ai/docs/framework/how-to-configure-pyproject-toml.html
|
|
4
|
+
# =====================================================================
|
|
5
|
+
|
|
6
|
+
[build-system]
|
|
7
|
+
requires = ["hatchling"]
|
|
8
|
+
build-backend = "hatchling.build"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "$package_name"
|
|
12
|
+
version = "1.0.0"
|
|
13
|
+
description = ""
|
|
14
|
+
license = "Apache-2.0"
|
|
15
|
+
# Dependencies for your Flower App
|
|
16
|
+
dependencies = [
|
|
17
|
+
"flwr[simulation]>=1.21.0",
|
|
18
|
+
"flwr-datasets[vision]>=0.5.0",
|
|
19
|
+
"torch==2.7.1",
|
|
20
|
+
"torchvision==0.22.1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["."]
|
|
25
|
+
|
|
26
|
+
[tool.flwr.app]
|
|
27
|
+
publisher = "$username"
|
|
28
|
+
|
|
29
|
+
# Point to your ServerApp and ClientApp objects
|
|
30
|
+
[tool.flwr.app.components]
|
|
31
|
+
serverapp = "$import_name.server_app:app"
|
|
32
|
+
clientapp = "$import_name.client_app:app"
|
|
33
|
+
|
|
34
|
+
# Custom config values accessible via `context.run_config`
|
|
35
|
+
[tool.flwr.app.config]
|
|
36
|
+
num-server-rounds = 3
|
|
37
|
+
fraction-train = 0.5
|
|
38
|
+
local-epochs = 1
|
|
39
|
+
lr = 0.01
|
|
40
|
+
|
|
41
|
+
# Default federation to use when running the app
|
|
42
|
+
[tool.flwr.federations]
|
|
43
|
+
default = "local-simulation"
|
|
44
|
+
|
|
45
|
+
# Local simulation federation with 10 virtual SuperNodes
|
|
46
|
+
[tool.flwr.federations.local-simulation]
|
|
47
|
+
options.num-supernodes = 10
|
|
48
|
+
|
|
49
|
+
# Remote federation example for use with SuperLink
|
|
50
|
+
[tool.flwr.federations.remote-federation]
|
|
51
|
+
address = "<SUPERLINK-ADDRESS>:<PORT>"
|
|
52
|
+
insecure = true # Remove this line to enable TLS
|
|
53
|
+
# root-certificates = "<PATH/TO/ca.crt>" # For TLS setup
|
flwr/cli/run/run.py
CHANGED
|
@@ -30,7 +30,7 @@ from flwr.cli.config_utils import (
|
|
|
30
30
|
process_loaded_project_config,
|
|
31
31
|
validate_federation_in_project_config,
|
|
32
32
|
)
|
|
33
|
-
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
33
|
+
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE, RUN_CONFIG_HELP_MESSAGE
|
|
34
34
|
from flwr.common.config import (
|
|
35
35
|
flatten_dict,
|
|
36
36
|
get_metadata_from_config,
|
|
@@ -41,8 +41,8 @@ from flwr.common.constant import CliOutputFormat
|
|
|
41
41
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
42
42
|
from flwr.common.serde import config_record_to_proto, fab_to_proto, user_config_to_proto
|
|
43
43
|
from flwr.common.typing import Fab
|
|
44
|
-
from flwr.proto.
|
|
45
|
-
from flwr.proto.
|
|
44
|
+
from flwr.proto.control_pb2 import StartRunRequest # pylint: disable=E0611
|
|
45
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
46
46
|
|
|
47
47
|
from ..log import start_stream
|
|
48
48
|
from ..utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
|
|
@@ -65,11 +65,7 @@ def run(
|
|
|
65
65
|
typer.Option(
|
|
66
66
|
"--run-config",
|
|
67
67
|
"-c",
|
|
68
|
-
help=
|
|
69
|
-
"`--run-config 'key1=value1 key2=value2' --run-config 'key3=value3'`\n\n"
|
|
70
|
-
"Values can be of any type supported in TOML, such as bool, int, "
|
|
71
|
-
"float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
|
|
72
|
-
"in this example) exist in `pyproject.toml` for proper overriding.",
|
|
68
|
+
help=RUN_CONFIG_HELP_MESSAGE,
|
|
73
69
|
),
|
|
74
70
|
] = None,
|
|
75
71
|
federation_config_overrides: Annotated[
|
|
@@ -112,7 +108,7 @@ def run(
|
|
|
112
108
|
)
|
|
113
109
|
|
|
114
110
|
if "address" in federation_config:
|
|
115
|
-
|
|
111
|
+
_run_with_control_api(
|
|
116
112
|
app,
|
|
117
113
|
federation,
|
|
118
114
|
federation_config,
|
|
@@ -121,7 +117,7 @@ def run(
|
|
|
121
117
|
output_format,
|
|
122
118
|
)
|
|
123
119
|
else:
|
|
124
|
-
|
|
120
|
+
_run_without_control_api(
|
|
125
121
|
app, federation_config, run_config_overrides, federation
|
|
126
122
|
)
|
|
127
123
|
except (typer.Exit, Exception) as err: # pylint: disable=broad-except
|
|
@@ -142,7 +138,7 @@ def run(
|
|
|
142
138
|
|
|
143
139
|
|
|
144
140
|
# pylint: disable-next=R0913, R0914, R0917
|
|
145
|
-
def
|
|
141
|
+
def _run_with_control_api(
|
|
146
142
|
app: Path,
|
|
147
143
|
federation: str,
|
|
148
144
|
federation_config: dict[str, Any],
|
|
@@ -154,7 +150,7 @@ def _run_with_exec_api(
|
|
|
154
150
|
try:
|
|
155
151
|
auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
|
|
156
152
|
channel = init_channel(app, federation_config, auth_plugin)
|
|
157
|
-
stub =
|
|
153
|
+
stub = ControlStub(channel)
|
|
158
154
|
|
|
159
155
|
fab_bytes, fab_hash, config = build_fab(app)
|
|
160
156
|
fab_id, fab_version = get_metadata_from_config(config)
|
|
@@ -203,7 +199,7 @@ def _run_with_exec_api(
|
|
|
203
199
|
channel.close()
|
|
204
200
|
|
|
205
201
|
|
|
206
|
-
def
|
|
202
|
+
def _run_without_control_api(
|
|
207
203
|
app: Optional[Path],
|
|
208
204
|
federation_config: dict[str, Any],
|
|
209
205
|
config_overrides: Optional[list[str]],
|
flwr/cli/stop.py
CHANGED
|
@@ -32,8 +32,11 @@ from flwr.cli.config_utils import (
|
|
|
32
32
|
from flwr.cli.constant import FEDERATION_CONFIG_HELP_MESSAGE
|
|
33
33
|
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
|
34
34
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
|
35
|
-
from flwr.proto.
|
|
36
|
-
|
|
35
|
+
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
36
|
+
StopRunRequest,
|
|
37
|
+
StopRunResponse,
|
|
38
|
+
)
|
|
39
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
37
40
|
|
|
38
41
|
from .utils import flwr_cli_grpc_exc_handler, init_channel, try_obtain_cli_auth_plugin
|
|
39
42
|
|
|
@@ -88,7 +91,7 @@ def stop( # pylint: disable=R0914
|
|
|
88
91
|
try:
|
|
89
92
|
auth_plugin = try_obtain_cli_auth_plugin(app, federation, federation_config)
|
|
90
93
|
channel = init_channel(app, federation_config, auth_plugin)
|
|
91
|
-
stub =
|
|
94
|
+
stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
|
|
92
95
|
|
|
93
96
|
typer.secho(f"✋ Stopping run ID {run_id}...", fg=typer.colors.GREEN)
|
|
94
97
|
_stop_run(stub=stub, run_id=run_id, output_format=output_format)
|
|
@@ -120,7 +123,7 @@ def stop( # pylint: disable=R0914
|
|
|
120
123
|
captured_output.close()
|
|
121
124
|
|
|
122
125
|
|
|
123
|
-
def _stop_run(stub:
|
|
126
|
+
def _stop_run(stub: ControlStub, run_id: int, output_format: str) -> None:
|
|
124
127
|
"""Stop a run."""
|
|
125
128
|
with flwr_cli_grpc_exc_handler():
|
|
126
129
|
response: StopRunResponse = stub.StopRun(request=StopRunRequest(run_id=run_id))
|
flwr/cli/utils.py
CHANGED
|
@@ -32,6 +32,7 @@ from flwr.common.constant import (
|
|
|
32
32
|
AUTH_TYPE_JSON_KEY,
|
|
33
33
|
CREDENTIALS_DIR,
|
|
34
34
|
FLWR_DIR,
|
|
35
|
+
NO_USER_AUTH_MESSAGE,
|
|
35
36
|
RUN_ID_NOT_FOUND_MESSAGE,
|
|
36
37
|
)
|
|
37
38
|
from flwr.common.grpc import (
|
|
@@ -259,7 +260,7 @@ def try_obtain_cli_auth_plugin(
|
|
|
259
260
|
def init_channel(
|
|
260
261
|
app: Path, federation_config: dict[str, Any], auth_plugin: Optional[CliAuthPlugin]
|
|
261
262
|
) -> grpc.Channel:
|
|
262
|
-
"""Initialize gRPC channel to the
|
|
263
|
+
"""Initialize gRPC channel to the Control API."""
|
|
263
264
|
insecure, root_certificates_bytes = validate_certificate_in_federation_config(
|
|
264
265
|
app, federation_config
|
|
265
266
|
)
|
|
@@ -312,11 +313,21 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
|
|
312
313
|
)
|
|
313
314
|
raise typer.Exit(code=1) from None
|
|
314
315
|
if e.code() == grpc.StatusCode.UNIMPLEMENTED:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
if e.details() == NO_USER_AUTH_MESSAGE: # pylint: disable=E1101
|
|
317
|
+
typer.secho(
|
|
318
|
+
"❌ User authentication is not enabled on this SuperLink.",
|
|
319
|
+
fg=typer.colors.RED,
|
|
320
|
+
bold=True,
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
typer.secho(
|
|
324
|
+
"❌ The SuperLink cannot process this request. Please verify that "
|
|
325
|
+
"you set the address to its Control API endpoint correctly in your "
|
|
326
|
+
"`pyproject.toml`, and ensure that the Flower versions used by "
|
|
327
|
+
"the CLI and SuperLink are compatible.",
|
|
328
|
+
fg=typer.colors.RED,
|
|
329
|
+
bold=True,
|
|
330
|
+
)
|
|
320
331
|
raise typer.Exit(code=1) from None
|
|
321
332
|
if e.code() == grpc.StatusCode.PERMISSION_DENIED:
|
|
322
333
|
typer.secho(
|
|
@@ -324,7 +335,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
|
|
324
335
|
fg=typer.colors.RED,
|
|
325
336
|
bold=True,
|
|
326
337
|
)
|
|
327
|
-
# pylint: disable=E1101
|
|
338
|
+
# pylint: disable-next=E1101
|
|
328
339
|
typer.secho(e.details(), fg=typer.colors.RED, bold=True)
|
|
329
340
|
raise typer.Exit(code=1) from None
|
|
330
341
|
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
@@ -337,7 +348,7 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
|
|
|
337
348
|
raise typer.Exit(code=1) from None
|
|
338
349
|
if (
|
|
339
350
|
e.code() == grpc.StatusCode.NOT_FOUND
|
|
340
|
-
and e.details() == RUN_ID_NOT_FOUND_MESSAGE
|
|
351
|
+
and e.details() == RUN_ID_NOT_FOUND_MESSAGE # pylint: disable=E1101
|
|
341
352
|
):
|
|
342
353
|
typer.secho(
|
|
343
354
|
"❌ Run ID not found.",
|
|
@@ -40,7 +40,7 @@ from flwr.common.secure_aggregation.crypto.symmetric_encryption import (
|
|
|
40
40
|
generate_key_pairs,
|
|
41
41
|
)
|
|
42
42
|
from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
|
|
43
|
-
from flwr.common.typing import Fab, Run
|
|
43
|
+
from flwr.common.typing import Fab, Run
|
|
44
44
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
45
45
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
46
46
|
CreateNodeRequest,
|
|
@@ -157,17 +157,6 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
157
157
|
stub = adapter_cls(channel)
|
|
158
158
|
node: Optional[Node] = None
|
|
159
159
|
|
|
160
|
-
def _should_giveup_fn(e: Exception) -> bool:
|
|
161
|
-
if e.code() == grpc.StatusCode.PERMISSION_DENIED: # type: ignore
|
|
162
|
-
raise RunNotRunningException
|
|
163
|
-
if e.code() == grpc.StatusCode.UNAVAILABLE: # type: ignore
|
|
164
|
-
return False
|
|
165
|
-
return True
|
|
166
|
-
|
|
167
|
-
# Restrict retries to cases where the status code is UNAVAILABLE
|
|
168
|
-
# If the status code is PERMISSION_DENIED, additionally raise RunNotRunningException
|
|
169
|
-
retry_invoker.should_giveup = _should_giveup_fn
|
|
170
|
-
|
|
171
160
|
# Wrap stub
|
|
172
161
|
_wrap_stub(stub, retry_invoker)
|
|
173
162
|
###########################################################################
|
|
@@ -176,6 +176,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
176
176
|
# Shared variables for inner functions
|
|
177
177
|
node: Optional[Node] = None
|
|
178
178
|
|
|
179
|
+
# Remove should_giveup from RetryInvoker as REST does not support gRPC status codes
|
|
180
|
+
retry_invoker.should_giveup = None
|
|
181
|
+
|
|
179
182
|
###########################################################################
|
|
180
183
|
# heartbeat/create_node/delete_node/receive/send/get_run functions
|
|
181
184
|
###########################################################################
|
flwr/clientapp/__init__.py
CHANGED
|
@@ -13,3 +13,13 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Public Flower ClientApp APIs."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from flwr.client.client_app import ClientApp
|
|
19
|
+
|
|
20
|
+
from . import mod
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ClientApp",
|
|
24
|
+
"mod",
|
|
25
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright 2025 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 Built-in Mods."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from flwr.client.mod.comms_mods import arrays_size_mod, message_size_mod
|
|
19
|
+
|
|
20
|
+
from .centraldp_mods import fixedclipping_mod
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"arrays_size_mod",
|
|
24
|
+
"fixedclipping_mod",
|
|
25
|
+
"message_size_mod",
|
|
26
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
"""Clipping modifiers for central DP with client-side clipping."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from logging import INFO, WARN
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
from flwr.client.typing import ClientAppCallable
|
|
23
|
+
from flwr.common import Array, ArrayRecord, Context, Message, MessageType, log
|
|
24
|
+
from flwr.common.differential_privacy import compute_clip_model_update
|
|
25
|
+
from flwr.common.differential_privacy_constants import KEY_CLIPPING_NORM
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# pylint: disable=too-many-return-statements
|
|
29
|
+
def fixedclipping_mod(
|
|
30
|
+
msg: Message, ctxt: Context, call_next: ClientAppCallable
|
|
31
|
+
) -> Message:
|
|
32
|
+
"""Client-side fixed clipping modifier.
|
|
33
|
+
|
|
34
|
+
This mod needs to be used with the `DifferentialPrivacyClientSideFixedClipping`
|
|
35
|
+
server-side strategy wrapper.
|
|
36
|
+
|
|
37
|
+
The wrapper sends the clipping_norm value to the client.
|
|
38
|
+
|
|
39
|
+
This mod clips the client model updates before sending them to the server.
|
|
40
|
+
|
|
41
|
+
It operates on messages of type `MessageType.TRAIN`.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
Consider the order of mods when using multiple.
|
|
46
|
+
|
|
47
|
+
Typically, fixedclipping_mod should be the last to operate on params.
|
|
48
|
+
"""
|
|
49
|
+
if msg.metadata.message_type != MessageType.TRAIN:
|
|
50
|
+
return call_next(msg, ctxt)
|
|
51
|
+
|
|
52
|
+
if len(msg.content.array_records) != 1:
|
|
53
|
+
log(
|
|
54
|
+
WARN,
|
|
55
|
+
"fixedclipping_mod is designed to work with a single ArrayRecord. "
|
|
56
|
+
"Skipping.",
|
|
57
|
+
)
|
|
58
|
+
return call_next(msg, ctxt)
|
|
59
|
+
|
|
60
|
+
if len(msg.content.config_records) != 1:
|
|
61
|
+
log(
|
|
62
|
+
WARN,
|
|
63
|
+
"fixedclipping_mod is designed to work with a single ConfigRecord. "
|
|
64
|
+
"Skipping.",
|
|
65
|
+
)
|
|
66
|
+
return call_next(msg, ctxt)
|
|
67
|
+
|
|
68
|
+
# Get keys in the single ConfigRecord
|
|
69
|
+
keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
|
|
70
|
+
if KEY_CLIPPING_NORM not in keys_in_config:
|
|
71
|
+
raise KeyError(
|
|
72
|
+
f"The {KEY_CLIPPING_NORM} value is not supplied by the "
|
|
73
|
+
f"`DifferentialPrivacyClientSideFixedClipping` wrapper at"
|
|
74
|
+
f" the server side."
|
|
75
|
+
)
|
|
76
|
+
# Record array record communicated to client and clipping norm
|
|
77
|
+
original_array_record = next(iter(msg.content.array_records.values()))
|
|
78
|
+
clipping_norm = cast(
|
|
79
|
+
float, next(iter(msg.content.config_records.values()))[KEY_CLIPPING_NORM]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Call inner app
|
|
83
|
+
out_msg = call_next(msg, ctxt)
|
|
84
|
+
|
|
85
|
+
# Check if the msg has error
|
|
86
|
+
if out_msg.has_error():
|
|
87
|
+
return out_msg
|
|
88
|
+
|
|
89
|
+
# Ensure there is a single ArrayRecord
|
|
90
|
+
if len(out_msg.content.array_records) != 1:
|
|
91
|
+
log(
|
|
92
|
+
WARN,
|
|
93
|
+
"fixedclipping_mod is designed to work with a single ArrayRecord. "
|
|
94
|
+
"Skipping.",
|
|
95
|
+
)
|
|
96
|
+
return out_msg
|
|
97
|
+
|
|
98
|
+
new_array_record_key, client_to_server_arrecord = next(
|
|
99
|
+
iter(out_msg.content.array_records.items())
|
|
100
|
+
)
|
|
101
|
+
# Ensure keys in returned ArrayRecord match those in the one sent from server
|
|
102
|
+
if set(original_array_record.keys()) != set(client_to_server_arrecord.keys()):
|
|
103
|
+
log(
|
|
104
|
+
WARN,
|
|
105
|
+
"fixedclipping_mod: Keys in ArrayRecord must match those from the model "
|
|
106
|
+
"that the ClientApp received. Skipping.",
|
|
107
|
+
)
|
|
108
|
+
return out_msg
|
|
109
|
+
|
|
110
|
+
client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
|
|
111
|
+
# Clip the client update
|
|
112
|
+
compute_clip_model_update(
|
|
113
|
+
param1=client_to_server_ndarrays,
|
|
114
|
+
param2=original_array_record.to_numpy_ndarrays(),
|
|
115
|
+
clipping_norm=clipping_norm,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
log(
|
|
119
|
+
INFO, "fixedclipping_mod: parameters are clipped by value: %.4f.", clipping_norm
|
|
120
|
+
)
|
|
121
|
+
# Replace outgoing ArrayRecord's Array while preserving their keys
|
|
122
|
+
out_msg.content.array_records[new_array_record_key] = ArrayRecord(
|
|
123
|
+
OrderedDict(
|
|
124
|
+
{
|
|
125
|
+
k: Array(v)
|
|
126
|
+
for k, v in zip(
|
|
127
|
+
client_to_server_arrecord.keys(), client_to_server_ndarrays
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return out_msg
|
flwr/common/args.py
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
19
19
|
import sys
|
|
20
|
-
from logging import DEBUG, ERROR, WARN
|
|
20
|
+
from logging import DEBUG, ERROR, INFO, WARN
|
|
21
21
|
from os.path import isfile
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Optional, Union
|
|
@@ -28,6 +28,12 @@ from flwr.common.logger import log
|
|
|
28
28
|
|
|
29
29
|
def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
|
|
30
30
|
"""Add common Flower arguments for flwr-*app to the provided parser."""
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--token",
|
|
33
|
+
type=str,
|
|
34
|
+
required=False,
|
|
35
|
+
help="Unique token generated by AppIo API for each app execution",
|
|
36
|
+
)
|
|
31
37
|
parser.add_argument(
|
|
32
38
|
"--flwr-dir",
|
|
33
39
|
default=None,
|
|
@@ -47,6 +53,18 @@ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
|
|
|
47
53
|
"is not encrypted. By default, the server runs with HTTPS enabled. "
|
|
48
54
|
"Use this flag only if you understand the risks.",
|
|
49
55
|
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--parent-pid",
|
|
58
|
+
type=int,
|
|
59
|
+
default=None,
|
|
60
|
+
help="The PID of the parent process. When set, the process will terminate "
|
|
61
|
+
"when the parent process exits.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--run-once",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="This flag is deprecated and will be removed in a future release.",
|
|
67
|
+
)
|
|
50
68
|
|
|
51
69
|
|
|
52
70
|
def try_obtain_root_certificates(
|
|
@@ -72,11 +90,7 @@ def try_obtain_root_certificates(
|
|
|
72
90
|
else:
|
|
73
91
|
# Load the certificates if provided, or load the system certificates
|
|
74
92
|
if root_cert_path is None:
|
|
75
|
-
log(
|
|
76
|
-
WARN,
|
|
77
|
-
"Both `--insecure` and `--root-certificates` were not set. "
|
|
78
|
-
"Using system certificates.",
|
|
79
|
-
)
|
|
93
|
+
log(INFO, "Using system certificates")
|
|
80
94
|
root_certificates = None
|
|
81
95
|
elif not isfile(root_cert_path):
|
|
82
96
|
log(ERROR, "Path argument `--root-certificates` does not point to a file.")
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from .auth_plugin import CliAuthPlugin as CliAuthPlugin
|
|
19
|
-
from .auth_plugin import
|
|
20
|
-
from .auth_plugin import
|
|
19
|
+
from .auth_plugin import ControlAuthPlugin as ControlAuthPlugin
|
|
20
|
+
from .auth_plugin import ControlAuthzPlugin as ControlAuthzPlugin
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"CliAuthPlugin",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"ControlAuthPlugin",
|
|
25
|
+
"ControlAuthzPlugin",
|
|
26
26
|
]
|
|
@@ -21,13 +21,13 @@ from pathlib import Path
|
|
|
21
21
|
from typing import Optional, Union
|
|
22
22
|
|
|
23
23
|
from flwr.common.typing import AccountInfo
|
|
24
|
-
from flwr.proto.
|
|
24
|
+
from flwr.proto.control_pb2_grpc import ControlStub
|
|
25
25
|
|
|
26
26
|
from ..typing import UserAuthCredentials, UserAuthLoginDetails
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class
|
|
30
|
-
"""Abstract Flower Auth Plugin class for
|
|
29
|
+
class ControlAuthPlugin(ABC):
|
|
30
|
+
"""Abstract Flower Auth Plugin class for ControlServicer.
|
|
31
31
|
|
|
32
32
|
Parameters
|
|
33
33
|
----------
|
|
@@ -69,8 +69,8 @@ class ExecAuthPlugin(ABC):
|
|
|
69
69
|
"""Refresh authentication tokens in the provided metadata."""
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
class
|
|
73
|
-
"""Abstract Flower Authorization Plugin class for
|
|
72
|
+
class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
|
|
73
|
+
"""Abstract Flower Authorization Plugin class for ControlServicer.
|
|
74
74
|
|
|
75
75
|
Parameters
|
|
76
76
|
----------
|
|
@@ -103,7 +103,7 @@ class CliAuthPlugin(ABC):
|
|
|
103
103
|
@abstractmethod
|
|
104
104
|
def login(
|
|
105
105
|
login_details: UserAuthLoginDetails,
|
|
106
|
-
|
|
106
|
+
control_stub: ControlStub,
|
|
107
107
|
) -> UserAuthCredentials:
|
|
108
108
|
"""Authenticate the user and retrieve authentication credentials.
|
|
109
109
|
|
|
@@ -111,7 +111,7 @@ class CliAuthPlugin(ABC):
|
|
|
111
111
|
----------
|
|
112
112
|
login_details : UserAuthLoginDetails
|
|
113
113
|
An object containing the user's login details.
|
|
114
|
-
|
|
114
|
+
control_stub : ControlStub
|
|
115
115
|
A stub for executing RPC calls to the server.
|
|
116
116
|
|
|
117
117
|
Returns
|
flwr/common/constant.py
CHANGED
|
@@ -35,7 +35,7 @@ CLIENTAPPIO_PORT = "9094"
|
|
|
35
35
|
SERVERAPPIO_PORT = "9091"
|
|
36
36
|
FLEETAPI_GRPC_RERE_PORT = "9092"
|
|
37
37
|
FLEETAPI_PORT = "9095"
|
|
38
|
-
|
|
38
|
+
CONTROL_API_PORT = "9093"
|
|
39
39
|
SIMULATIONIO_PORT = "9096"
|
|
40
40
|
# Octets
|
|
41
41
|
SERVER_OCTET = "0.0.0.0"
|
|
@@ -51,7 +51,7 @@ FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS = (
|
|
|
51
51
|
"[::]:8080" # IPv6 to keep start_server compatible
|
|
52
52
|
)
|
|
53
53
|
FLEET_API_REST_DEFAULT_ADDRESS = f"{SERVER_OCTET}:{FLEETAPI_PORT}"
|
|
54
|
-
|
|
54
|
+
CONTROL_API_DEFAULT_SERVER_ADDRESS = f"{SERVER_OCTET}:{CONTROL_API_PORT}"
|
|
55
55
|
SIMULATIONIO_API_DEFAULT_SERVER_ADDRESS = f"{SERVER_OCTET}:{SIMULATIONIO_PORT}"
|
|
56
56
|
SIMULATIONIO_API_DEFAULT_CLIENT_ADDRESS = f"{CLIENT_OCTET}:{SIMULATIONIO_PORT}"
|
|
57
57
|
|
|
@@ -103,7 +103,7 @@ ISOLATION_MODE_PROCESS = "process"
|
|
|
103
103
|
# Log streaming configurations
|
|
104
104
|
CONN_REFRESH_PERIOD = 60 # Stream connection refresh period
|
|
105
105
|
CONN_RECONNECT_INTERVAL = 0.5 # Reconnect interval between two stream connections
|
|
106
|
-
LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `
|
|
106
|
+
LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `ControlServicer.StreamLogs`
|
|
107
107
|
LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
|
|
108
108
|
|
|
109
109
|
# Retry configurations
|
|
@@ -152,8 +152,9 @@ PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
|
|
|
152
152
|
PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
#
|
|
155
|
+
# ControlServicer constants
|
|
156
156
|
RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
|
|
157
|
+
NO_USER_AUTH_MESSAGE = "ControlServicer initialized without user authentication"
|
|
157
158
|
|
|
158
159
|
|
|
159
160
|
class MessageType:
|
|
@@ -259,3 +260,21 @@ class EventLogWriterType:
|
|
|
259
260
|
def __new__(cls) -> EventLogWriterType:
|
|
260
261
|
"""Prevent instantiation."""
|
|
261
262
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class ExecPluginType:
|
|
266
|
+
"""SuperExec plugin types."""
|
|
267
|
+
|
|
268
|
+
CLIENT_APP = "clientapp"
|
|
269
|
+
SERVER_APP = "serverapp"
|
|
270
|
+
SIMULATION = "simulation"
|
|
271
|
+
|
|
272
|
+
def __new__(cls) -> ExecPluginType:
|
|
273
|
+
"""Prevent instantiation."""
|
|
274
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def all() -> list[str]:
|
|
278
|
+
"""Return all SuperExec plugin types."""
|
|
279
|
+
# Filter all constants (uppercase) of the class
|
|
280
|
+
return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
|
|
@@ -25,7 +25,7 @@ from flwr.common.typing import AccountInfo, LogEntry
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class EventLogWriterPlugin(ABC):
|
|
28
|
-
"""Abstract Flower Event Log Writer Plugin class for
|
|
28
|
+
"""Abstract Flower Event Log Writer Plugin class for ControlServicer."""
|
|
29
29
|
|
|
30
30
|
@abstractmethod
|
|
31
31
|
def __init__(self) -> None:
|
flwr/common/exit/__init__.py
CHANGED
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
from .exit import flwr_exit
|
|
19
19
|
from .exit_code import ExitCode
|
|
20
|
+
from .exit_handler import add_exit_handler
|
|
21
|
+
from .signal_handler import register_signal_handlers
|
|
20
22
|
|
|
21
23
|
__all__ = [
|
|
22
24
|
"ExitCode",
|
|
25
|
+
"add_exit_handler",
|
|
23
26
|
"flwr_exit",
|
|
27
|
+
"register_signal_handlers",
|
|
24
28
|
]
|