flwr-nightly 1.21.0.dev20250902__py3-none-any.whl → 1.21.0.dev20250903__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/constant.py +25 -8
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -8
- flwr/cli/run/run.py +2 -6
- flwr/clientapp/__init__.py +4 -0
- flwr/clientapp/centraldp_mods.py +132 -0
- flwr/common/exception.py +31 -0
- flwr/common/exit/exit_code.py +2 -0
- flwr/server/serverapp/app.py +41 -28
- flwr/serverapp/dp_fixed_clipping.py +352 -0
- flwr/serverapp/strategy/__init__.py +6 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +352 -0
- flwr/serverapp/strategy/result.py +76 -2
- flwr/serverapp/strategy/strategy.py +15 -20
- flwr/serverapp/strategy/strategy_utils.py +5 -1
- {flwr_nightly-1.21.0.dev20250902.dist-info → flwr_nightly-1.21.0.dev20250903.dist-info}/METADATA +1 -1
- {flwr_nightly-1.21.0.dev20250902.dist-info → flwr_nightly-1.21.0.dev20250903.dist-info}/RECORD +18 -14
- {flwr_nightly-1.21.0.dev20250902.dist-info → flwr_nightly-1.21.0.dev20250903.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.21.0.dev20250902.dist-info → flwr_nightly-1.21.0.dev20250903.dist-info}/entry_points.txt +0 -0
flwr/cli/constant.py
CHANGED
@@ -15,13 +15,30 @@
|
|
15
15
|
"""Constants for CLI commands."""
|
16
16
|
|
17
17
|
|
18
|
+
# General help message for config overrides
|
19
|
+
CONFIG_HELP_MESSAGE = (
|
20
|
+
"Override {0} values using one of the following formats:\n\n"
|
21
|
+
"--{1} '<k1>=<v1> <k2>=<v2>' | --{1} '<k1>=<v1>' --{1} '<k2>=<v2>'{2}\n\n"
|
22
|
+
"When providing key-value pairs, values can be of any type supported by TOML "
|
23
|
+
"(e.g., bool, int, float, string). The specified keys (<k1> and <k2> in the "
|
24
|
+
"example) must exist in the {0} under the `{3}` section of `pyproject.toml` to be "
|
25
|
+
"overridden.{4}"
|
26
|
+
)
|
27
|
+
|
28
|
+
# The help message for `--run-config` option
|
29
|
+
RUN_CONFIG_HELP_MESSAGE = CONFIG_HELP_MESSAGE.format(
|
30
|
+
"run configuration",
|
31
|
+
"run-config",
|
32
|
+
" | --run-config <path/to/your/toml>",
|
33
|
+
"[tool.flwr.app.config]",
|
34
|
+
" Alternatively, provide a TOML file containing overrides.",
|
35
|
+
)
|
36
|
+
|
18
37
|
# The help message for `--federation-config` option
|
19
|
-
FEDERATION_CONFIG_HELP_MESSAGE = (
|
20
|
-
"
|
21
|
-
"
|
22
|
-
"
|
23
|
-
"
|
24
|
-
"
|
25
|
-
"`[tool.flwr.federations.<YOUR_FEDERATION>]` table of the `pyproject.toml` "
|
26
|
-
"for proper overriding."
|
38
|
+
FEDERATION_CONFIG_HELP_MESSAGE = CONFIG_HELP_MESSAGE.format(
|
39
|
+
"federation configuration",
|
40
|
+
"federation-config",
|
41
|
+
"",
|
42
|
+
"[tool.flwr.federations.<YOUR-FEDERATION>]",
|
43
|
+
"",
|
27
44
|
)
|
@@ -1,7 +1,5 @@
|
|
1
1
|
"""$project_name: A Flower / $framework_str app."""
|
2
2
|
|
3
|
-
from pprint import pprint
|
4
|
-
|
5
3
|
import torch
|
6
4
|
from flwr.common import ArrayRecord, ConfigRecord, Context
|
7
5
|
from flwr.server import Grid, ServerApp
|
@@ -37,12 +35,6 @@ def main(grid: Grid, context: Context) -> None:
|
|
37
35
|
num_rounds=num_rounds,
|
38
36
|
)
|
39
37
|
|
40
|
-
# Log resulting metrics
|
41
|
-
print("\nDistributed train metrics:")
|
42
|
-
pprint(result.train_metrics_clientapp)
|
43
|
-
print("\nDistributed evaluate metrics:")
|
44
|
-
pprint(result.evaluate_metrics_clientapp)
|
45
|
-
|
46
38
|
# Save final model to disk
|
47
39
|
print("\nSaving final model to disk...")
|
48
40
|
state_dict = result.arrays.to_torch_state_dict()
|
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,
|
@@ -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[
|
flwr/clientapp/__init__.py
CHANGED
@@ -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/exception.py
ADDED
@@ -0,0 +1,31 @@
|
|
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 application exceptions."""
|
16
|
+
|
17
|
+
|
18
|
+
class AppExitException(Exception):
|
19
|
+
"""Base exception for all application-level errors in ServerApp and ClientApp.
|
20
|
+
|
21
|
+
When raised, the process will exit and report a telemetry event with the associated
|
22
|
+
exit code.
|
23
|
+
"""
|
24
|
+
|
25
|
+
# Default exit code — subclasses must override
|
26
|
+
exit_code = -1
|
27
|
+
|
28
|
+
def __init_subclass__(cls) -> None:
|
29
|
+
"""Ensure subclasses override the exit_code attribute."""
|
30
|
+
if cls.exit_code == -1:
|
31
|
+
raise ValueError("Subclasses must override the exit_code attribute.")
|
flwr/common/exit/exit_code.py
CHANGED
@@ -36,6 +36,7 @@ class ExitCode:
|
|
36
36
|
|
37
37
|
# ServerApp-specific exit codes (200-299)
|
38
38
|
SERVERAPP_STRATEGY_PRECONDITION_UNMET = 200
|
39
|
+
SERVERAPP_EXCEPTION = 201
|
39
40
|
SERVERAPP_STRATEGY_AGGREGATION_ERROR = 202
|
40
41
|
|
41
42
|
# SuperNode-specific exit codes (300-399)
|
@@ -90,6 +91,7 @@ EXIT_CODE_HELP = {
|
|
90
91
|
"perform weighted average (e.g. in FedAvg) please ensure the returned "
|
91
92
|
"MetricRecord from ClientApps do include this key."
|
92
93
|
),
|
94
|
+
ExitCode.SERVERAPP_EXCEPTION: "An unhandled exception occurred in the ServerApp.",
|
93
95
|
ExitCode.SERVERAPP_STRATEGY_AGGREGATION_ERROR: (
|
94
96
|
"The strategy encountered an error during aggregation. Please check the logs "
|
95
97
|
"for more details."
|
flwr/server/serverapp/app.py
CHANGED
@@ -37,7 +37,8 @@ from flwr.common.constant import (
|
|
37
37
|
Status,
|
38
38
|
SubStatus,
|
39
39
|
)
|
40
|
-
from flwr.common.
|
40
|
+
from flwr.common.exception import AppExitException
|
41
|
+
from flwr.common.exit import ExitCode, add_exit_handler, flwr_exit
|
41
42
|
from flwr.common.heartbeat import HeartbeatSender, get_grpc_app_heartbeat_fn
|
42
43
|
from flwr.common.logger import (
|
43
44
|
log,
|
@@ -133,12 +134,34 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
|
|
133
134
|
# Resolve directory where FABs are installed
|
134
135
|
flwr_dir_ = get_flwr_dir(flwr_dir)
|
135
136
|
log_uploader = None
|
136
|
-
success = True
|
137
137
|
hash_run_id = None
|
138
138
|
run_status = None
|
139
139
|
heartbeat_sender = None
|
140
140
|
grid = None
|
141
141
|
context = None
|
142
|
+
exit_code = ExitCode.SUCCESS
|
143
|
+
|
144
|
+
def on_exit() -> None:
|
145
|
+
# Stop heartbeat sender
|
146
|
+
if heartbeat_sender:
|
147
|
+
heartbeat_sender.stop()
|
148
|
+
|
149
|
+
# Stop log uploader for this run and upload final logs
|
150
|
+
if log_uploader:
|
151
|
+
stop_log_uploader(log_queue, log_uploader)
|
152
|
+
|
153
|
+
# Update run status
|
154
|
+
if run_status and grid:
|
155
|
+
run_status_proto = run_status_to_proto(run_status)
|
156
|
+
grid._stub.UpdateRunStatus(
|
157
|
+
UpdateRunStatusRequest(run_id=run.run_id, run_status=run_status_proto)
|
158
|
+
)
|
159
|
+
|
160
|
+
# Close the Grpc connection
|
161
|
+
if grid:
|
162
|
+
grid.close()
|
163
|
+
|
164
|
+
add_exit_handler(on_exit)
|
142
165
|
|
143
166
|
try:
|
144
167
|
# Initialize the GrpcGrid
|
@@ -229,43 +252,33 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
|
|
229
252
|
_ = grid._stub.PushAppOutputs(out_req)
|
230
253
|
|
231
254
|
run_status = RunStatus(Status.FINISHED, SubStatus.COMPLETED, "")
|
255
|
+
|
256
|
+
# Raised when the run is already stopped by the user
|
232
257
|
except RunNotRunningException:
|
233
258
|
log(INFO, "")
|
234
259
|
log(INFO, "Run ID %s stopped.", run.run_id)
|
235
260
|
log(INFO, "")
|
236
261
|
run_status = None
|
237
|
-
|
262
|
+
# No need to update the exit code since this is expected behavior
|
238
263
|
|
239
264
|
except Exception as ex: # pylint: disable=broad-exception-caught
|
240
265
|
exc_entity = "ServerApp"
|
241
266
|
log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
|
242
267
|
run_status = RunStatus(Status.FINISHED, SubStatus.FAILED, str(ex))
|
243
|
-
success = False
|
244
268
|
|
245
|
-
|
246
|
-
#
|
247
|
-
if
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
UpdateRunStatusRequest(run_id=run.run_id, run_status=run_status_proto)
|
259
|
-
)
|
260
|
-
|
261
|
-
# Close the Grpc connection
|
262
|
-
if grid:
|
263
|
-
grid.close()
|
264
|
-
|
265
|
-
event(
|
266
|
-
EventType.FLWR_SERVERAPP_RUN_LEAVE,
|
267
|
-
event_details={"run-id-hash": hash_run_id, "success": success},
|
268
|
-
)
|
269
|
+
# Set exit code
|
270
|
+
exit_code = ExitCode.SERVERAPP_EXCEPTION # General exit code
|
271
|
+
if isinstance(ex, AppExitException):
|
272
|
+
exit_code = ex.exit_code
|
273
|
+
|
274
|
+
flwr_exit(
|
275
|
+
code=exit_code,
|
276
|
+
event_type=EventType.FLWR_SERVERAPP_RUN_LEAVE,
|
277
|
+
event_details={
|
278
|
+
"run-id-hash": hash_run_id,
|
279
|
+
"success": exit_code == ExitCode.SUCCESS,
|
280
|
+
},
|
281
|
+
)
|
269
282
|
|
270
283
|
|
271
284
|
def _parse_args_run_flwr_serverapp() -> argparse.ArgumentParser:
|