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 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
- "Override federation configuration values in the format:\n\n"
21
- "`--federation-config 'key1=value1 key2=value2' --federation-config "
22
- "'key3=value3'`\n\nValues can be of any type supported in TOML, such as "
23
- "bool, int, float, or string. Ensure that the keys (`key1`, `key2`, `key3` "
24
- "in this example) exist in the federation configuration under the "
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="Override run configuration values in the format:\n\n"
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[
@@ -13,3 +13,7 @@
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
15
  """Public Flower ClientApp APIs."""
16
+
17
+ from .centraldp_mods import fixedclipping_mod
18
+
19
+ __all__ = ["fixedclipping_mod"]
@@ -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
@@ -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.")
@@ -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."
@@ -37,7 +37,8 @@ from flwr.common.constant import (
37
37
  Status,
38
38
  SubStatus,
39
39
  )
40
- from flwr.common.exit import ExitCode, flwr_exit
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
- success = False
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
- finally:
246
- # Stop heartbeat sender
247
- if heartbeat_sender:
248
- heartbeat_sender.stop()
249
-
250
- # Stop log uploader for this run and upload final logs
251
- if log_uploader:
252
- stop_log_uploader(log_queue, log_uploader)
253
-
254
- # Update run status
255
- if run_status and grid:
256
- run_status_proto = run_status_to_proto(run_status)
257
- grid._stub.UpdateRunStatus(
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: