flwr-nightly 1.8.0.dev20240310__py3-none-any.whl → 1.8.0.dev20240311__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/new/new.py CHANGED
@@ -22,7 +22,7 @@ from typing import Dict, Optional
22
22
  import typer
23
23
  from typing_extensions import Annotated
24
24
 
25
- from ..utils import prompt_options
25
+ from ..utils import prompt_options, prompt_text
26
26
 
27
27
 
28
28
  class MlFramework(str, Enum):
@@ -72,9 +72,9 @@ def render_and_create(file_path: str, template: str, context: Dict[str, str]) ->
72
72
 
73
73
  def new(
74
74
  project_name: Annotated[
75
- str,
75
+ Optional[str],
76
76
  typer.Argument(metavar="project_name", help="The name of the project"),
77
- ],
77
+ ] = None,
78
78
  framework: Annotated[
79
79
  Optional[MlFramework],
80
80
  typer.Option(case_sensitive=False, help="The ML framework to use"),
@@ -83,6 +83,9 @@ def new(
83
83
  """Create new Flower project."""
84
84
  print(f"Creating Flower project {project_name}...")
85
85
 
86
+ if project_name is None:
87
+ project_name = prompt_text("Please provide project name")
88
+
86
89
  if framework is not None:
87
90
  framework_str = str(framework.value)
88
91
  else:
flwr/cli/utils.py CHANGED
@@ -14,11 +14,24 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface utils."""
16
16
 
17
- from typing import List
17
+ from typing import List, cast
18
18
 
19
19
  import typer
20
20
 
21
21
 
22
+ def prompt_text(text: str) -> str:
23
+ """Ask user to enter text input."""
24
+ while True:
25
+ result = typer.prompt(
26
+ typer.style(f"\n💬 {text}", fg=typer.colors.MAGENTA, bold=True)
27
+ )
28
+ if len(result) > 0:
29
+ break
30
+ print(typer.style("❌ Invalid entry", fg=typer.colors.RED, bold=True))
31
+
32
+ return cast(str, result)
33
+
34
+
22
35
  def prompt_options(text: str, options: List[str]) -> str:
23
36
  """Ask user to select one of the given options and return the selected item."""
24
37
  # Turn options into a list with index as in " [ 0] quickstart-pytorch"
flwr/client/app.py CHANGED
@@ -456,7 +456,19 @@ def _start_client_internal(
456
456
  time.sleep(3) # Wait for 3s before asking again
457
457
  continue
458
458
 
459
- log(INFO, "Received message")
459
+ log(INFO, "")
460
+ log(
461
+ INFO,
462
+ "[RUN %s, ROUND %s]",
463
+ message.metadata.run_id,
464
+ message.metadata.group_id,
465
+ )
466
+ log(
467
+ INFO,
468
+ "Received: %s message %s",
469
+ message.metadata.message_type,
470
+ message.metadata.message_id,
471
+ )
460
472
 
461
473
  # Handle control message
462
474
  out_message, sleep_duration = handle_control_message(message)
@@ -484,7 +496,18 @@ def _start_client_internal(
484
496
 
485
497
  # Send
486
498
  send(out_message)
487
- log(INFO, "Sent reply")
499
+ log(
500
+ INFO,
501
+ "[RUN %s, ROUND %s]",
502
+ out_message.metadata.run_id,
503
+ out_message.metadata.group_id,
504
+ )
505
+ log(
506
+ INFO,
507
+ "Sent: %s reply to message %s",
508
+ out_message.metadata.message_type,
509
+ message.metadata.message_id,
510
+ )
488
511
 
489
512
  # Unregister node
490
513
  if delete_node is not None:
@@ -17,7 +17,7 @@
17
17
 
18
18
  from .centraldp_mods import adaptiveclipping_mod, fixedclipping_mod
19
19
  from .localdp_mod import LocalDpMod
20
- from .secure_aggregation.secaggplus_mod import secaggplus_mod
20
+ from .secure_aggregation import secagg_mod, secaggplus_mod
21
21
  from .utils import make_ffn
22
22
 
23
23
  __all__ = [
@@ -25,5 +25,6 @@ __all__ = [
25
25
  "fixedclipping_mod",
26
26
  "LocalDpMod",
27
27
  "make_ffn",
28
+ "secagg_mod",
28
29
  "secaggplus_mod",
29
30
  ]
@@ -15,8 +15,10 @@
15
15
  """Secure Aggregation mods."""
16
16
 
17
17
 
18
+ from .secagg_mod import secagg_mod
18
19
  from .secaggplus_mod import secaggplus_mod
19
20
 
20
21
  __all__ = [
22
+ "secagg_mod",
21
23
  "secaggplus_mod",
22
24
  ]
@@ -0,0 +1,30 @@
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
+ """Modifier for the SecAgg protocol."""
16
+
17
+
18
+ from flwr.client.typing import ClientAppCallable
19
+ from flwr.common import Context, Message
20
+
21
+ from .secaggplus_mod import secaggplus_mod
22
+
23
+
24
+ def secagg_mod(
25
+ msg: Message,
26
+ ctxt: Context,
27
+ call_next: ClientAppCallable,
28
+ ) -> Message:
29
+ """Handle incoming message and return results, following the SecAgg protocol."""
30
+ return secaggplus_mod(msg, ctxt, call_next)
@@ -178,9 +178,9 @@ def secaggplus_mod(
178
178
  res = _setup(state, configs)
179
179
  elif state.current_stage == Stage.SHARE_KEYS:
180
180
  res = _share_keys(state, configs)
181
- elif state.current_stage == Stage.COLLECT_MASKED_INPUT:
181
+ elif state.current_stage == Stage.COLLECT_MASKED_VECTORS:
182
182
  fit = _get_fit_fn(msg, ctxt, call_next)
183
- res = _collect_masked_input(state, configs, fit)
183
+ res = _collect_masked_vectors(state, configs, fit)
184
184
  elif state.current_stage == Stage.UNMASK:
185
185
  res = _unmask(state, configs)
186
186
  else:
@@ -199,7 +199,7 @@ def check_stage(current_stage: str, configs: ConfigsRecord) -> None:
199
199
  # Check the existence of Config.STAGE
200
200
  if Key.STAGE not in configs:
201
201
  raise KeyError(
202
- f"The required key '{Key.STAGE}' is missing from the input `named_values`."
202
+ f"The required key '{Key.STAGE}' is missing from the ConfigsRecord."
203
203
  )
204
204
 
205
205
  # Check the value type of the Config.STAGE
@@ -215,7 +215,7 @@ def check_stage(current_stage: str, configs: ConfigsRecord) -> None:
215
215
  if current_stage != Stage.UNMASK:
216
216
  log(WARNING, "Restart from the setup stage")
217
217
  # If stage is not "setup",
218
- # the stage from `named_values` should be the expected next stage
218
+ # the stage from configs should be the expected next stage
219
219
  else:
220
220
  stages = Stage.all()
221
221
  expected_next_stage = stages[(stages.index(current_stage) + 1) % len(stages)]
@@ -229,7 +229,7 @@ def check_stage(current_stage: str, configs: ConfigsRecord) -> None:
229
229
  # pylint: disable-next=too-many-branches
230
230
  def check_configs(stage: str, configs: ConfigsRecord) -> None:
231
231
  """Check the validity of the configs."""
232
- # Check `named_values` for the setup stage
232
+ # Check configs for the setup stage
233
233
  if stage == Stage.SETUP:
234
234
  key_type_pairs = [
235
235
  (Key.SAMPLE_NUMBER, int),
@@ -243,7 +243,7 @@ def check_configs(stage: str, configs: ConfigsRecord) -> None:
243
243
  if key not in configs:
244
244
  raise KeyError(
245
245
  f"Stage {Stage.SETUP}: the required key '{key}' is "
246
- "missing from the input `named_values`."
246
+ "missing from the ConfigsRecord."
247
247
  )
248
248
  # Bool is a subclass of int in Python,
249
249
  # so `isinstance(v, int)` will return True even if v is a boolean.
@@ -266,7 +266,7 @@ def check_configs(stage: str, configs: ConfigsRecord) -> None:
266
266
  f"Stage {Stage.SHARE_KEYS}: "
267
267
  f"the value for the key '{key}' must be a list of two bytes."
268
268
  )
269
- elif stage == Stage.COLLECT_MASKED_INPUT:
269
+ elif stage == Stage.COLLECT_MASKED_VECTORS:
270
270
  key_type_pairs = [
271
271
  (Key.CIPHERTEXT_LIST, bytes),
272
272
  (Key.SOURCE_LIST, int),
@@ -274,9 +274,9 @@ def check_configs(stage: str, configs: ConfigsRecord) -> None:
274
274
  for key, expected_type in key_type_pairs:
275
275
  if key not in configs:
276
276
  raise KeyError(
277
- f"Stage {Stage.COLLECT_MASKED_INPUT}: "
277
+ f"Stage {Stage.COLLECT_MASKED_VECTORS}: "
278
278
  f"the required key '{key}' is "
279
- "missing from the input `named_values`."
279
+ "missing from the ConfigsRecord."
280
280
  )
281
281
  if not isinstance(configs[key], list) or any(
282
282
  elm
@@ -285,7 +285,7 @@ def check_configs(stage: str, configs: ConfigsRecord) -> None:
285
285
  if type(elm) is not expected_type
286
286
  ):
287
287
  raise TypeError(
288
- f"Stage {Stage.COLLECT_MASKED_INPUT}: "
288
+ f"Stage {Stage.COLLECT_MASKED_VECTORS}: "
289
289
  f"the value for the key '{key}' "
290
290
  f"must be of type List[{expected_type.__name__}]"
291
291
  )
@@ -299,7 +299,7 @@ def check_configs(stage: str, configs: ConfigsRecord) -> None:
299
299
  raise KeyError(
300
300
  f"Stage {Stage.UNMASK}: "
301
301
  f"the required key '{key}' is "
302
- "missing from the input `named_values`."
302
+ "missing from the ConfigsRecord."
303
303
  )
304
304
  if not isinstance(configs[key], list) or any(
305
305
  elm
@@ -414,7 +414,7 @@ def _share_keys(
414
414
 
415
415
 
416
416
  # pylint: disable-next=too-many-locals
417
- def _collect_masked_input(
417
+ def _collect_masked_vectors(
418
418
  state: SecAggPlusState,
419
419
  configs: ConfigsRecord,
420
420
  fit: Callable[[], FitRes],
flwr/common/logger.py CHANGED
@@ -168,11 +168,10 @@ def warn_experimental_feature(name: str) -> None:
168
168
  """Warn the user when they use an experimental feature."""
169
169
  log(
170
170
  WARN,
171
- """
172
- EXPERIMENTAL FEATURE: %s
171
+ """EXPERIMENTAL FEATURE: %s
173
172
 
174
- This is an experimental feature. It could change significantly or be removed
175
- entirely in future versions of Flower.
173
+ This is an experimental feature. It could change significantly or be removed
174
+ entirely in future versions of Flower.
176
175
  """,
177
176
  name,
178
177
  )
@@ -182,11 +181,10 @@ def warn_deprecated_feature(name: str) -> None:
182
181
  """Warn the user when they use a deprecated feature."""
183
182
  log(
184
183
  WARN,
185
- """
186
- DEPRECATED FEATURE: %s
184
+ """DEPRECATED FEATURE: %s
187
185
 
188
- This is a deprecated feature. It will be removed
189
- entirely in future versions of Flower.
186
+ This is a deprecated feature. It will be removed
187
+ entirely in future versions of Flower.
190
188
  """,
191
189
  name,
192
190
  )
@@ -27,9 +27,9 @@ class Stage:
27
27
 
28
28
  SETUP = "setup"
29
29
  SHARE_KEYS = "share_keys"
30
- COLLECT_MASKED_INPUT = "collect_masked_input"
30
+ COLLECT_MASKED_VECTORS = "collect_masked_vectors"
31
31
  UNMASK = "unmask"
32
- _stages = (SETUP, SHARE_KEYS, COLLECT_MASKED_INPUT, UNMASK)
32
+ _stages = (SETUP, SHARE_KEYS, COLLECT_MASKED_VECTORS, UNMASK)
33
33
 
34
34
  @classmethod
35
35
  def all(cls) -> tuple[str, str, str, str]:
@@ -122,7 +122,8 @@ class InMemoryState(State):
122
122
  task_res.task_id = str(task_id)
123
123
  task_res.task.created_at = created_at.isoformat()
124
124
  task_res.task.ttl = ttl.isoformat()
125
- self.task_res_store[task_id] = task_res
125
+ with self.lock:
126
+ self.task_res_store[task_id] = task_res
126
127
 
127
128
  # Return the new task_id
128
129
  return task_id
@@ -132,46 +133,47 @@ class InMemoryState(State):
132
133
  if limit is not None and limit < 1:
133
134
  raise AssertionError("`limit` must be >= 1")
134
135
 
135
- # Find TaskRes that were not delivered yet
136
- task_res_list: List[TaskRes] = []
137
- for _, task_res in self.task_res_store.items():
138
- if (
139
- UUID(task_res.task.ancestry[0]) in task_ids
140
- and task_res.task.delivered_at == ""
141
- ):
142
- task_res_list.append(task_res)
143
- if limit and len(task_res_list) == limit:
144
- break
136
+ with self.lock:
137
+ # Find TaskRes that were not delivered yet
138
+ task_res_list: List[TaskRes] = []
139
+ for _, task_res in self.task_res_store.items():
140
+ if (
141
+ UUID(task_res.task.ancestry[0]) in task_ids
142
+ and task_res.task.delivered_at == ""
143
+ ):
144
+ task_res_list.append(task_res)
145
+ if limit and len(task_res_list) == limit:
146
+ break
145
147
 
146
- # Mark all of them as delivered
147
- delivered_at = now().isoformat()
148
- for task_res in task_res_list:
149
- task_res.task.delivered_at = delivered_at
148
+ # Mark all of them as delivered
149
+ delivered_at = now().isoformat()
150
+ for task_res in task_res_list:
151
+ task_res.task.delivered_at = delivered_at
150
152
 
151
- # Return TaskRes
152
- return task_res_list
153
+ # Return TaskRes
154
+ return task_res_list
153
155
 
154
156
  def delete_tasks(self, task_ids: Set[UUID]) -> None:
155
157
  """Delete all delivered TaskIns/TaskRes pairs."""
156
158
  task_ins_to_be_deleted: Set[UUID] = set()
157
159
  task_res_to_be_deleted: Set[UUID] = set()
158
160
 
159
- for task_ins_id in task_ids:
160
- # Find the task_id of the matching task_res
161
- for task_res_id, task_res in self.task_res_store.items():
162
- if UUID(task_res.task.ancestry[0]) != task_ins_id:
163
- continue
164
- if task_res.task.delivered_at == "":
165
- continue
166
-
167
- task_ins_to_be_deleted.add(task_ins_id)
168
- task_res_to_be_deleted.add(task_res_id)
169
-
170
- for task_id in task_ins_to_be_deleted:
171
- with self.lock:
161
+ with self.lock:
162
+ for task_ins_id in task_ids:
163
+ # Find the task_id of the matching task_res
164
+ for task_res_id, task_res in self.task_res_store.items():
165
+ if UUID(task_res.task.ancestry[0]) != task_ins_id:
166
+ continue
167
+ if task_res.task.delivered_at == "":
168
+ continue
169
+
170
+ task_ins_to_be_deleted.add(task_ins_id)
171
+ task_res_to_be_deleted.add(task_res_id)
172
+
173
+ for task_id in task_ins_to_be_deleted:
172
174
  del self.task_ins_store[task_id]
173
- for task_id in task_res_to_be_deleted:
174
- del self.task_res_store[task_id]
175
+ for task_id in task_res_to_be_deleted:
176
+ del self.task_res_store[task_id]
175
177
 
176
178
  def num_task_ins(self) -> int:
177
179
  """Calculate the number of task_ins in store.
@@ -16,9 +16,10 @@
16
16
 
17
17
 
18
18
  from .default_workflows import DefaultWorkflow
19
- from .secure_aggregation import SecAggPlusWorkflow
19
+ from .secure_aggregation import SecAggPlusWorkflow, SecAggWorkflow
20
20
 
21
21
  __all__ = [
22
22
  "DefaultWorkflow",
23
23
  "SecAggPlusWorkflow",
24
+ "SecAggWorkflow",
24
25
  ]
@@ -15,8 +15,9 @@
15
15
  """Legacy default workflows."""
16
16
 
17
17
 
18
+ import io
18
19
  import timeit
19
- from logging import DEBUG, INFO
20
+ from logging import INFO
20
21
  from typing import Optional, cast
21
22
 
22
23
  import flwr.common.recordset_compat as compat
@@ -58,16 +59,18 @@ class DefaultWorkflow:
58
59
  )
59
60
 
60
61
  # Initialize parameters
62
+ log(INFO, "[INIT]")
61
63
  default_init_params_workflow(driver, context)
62
64
 
63
65
  # Run federated learning for num_rounds
64
- log(INFO, "FL starting")
65
66
  start_time = timeit.default_timer()
66
67
  cfg = ConfigsRecord()
67
68
  cfg[Key.START_TIME] = start_time
68
69
  context.state.configs_records[MAIN_CONFIGS_RECORD] = cfg
69
70
 
70
71
  for current_round in range(1, context.config.num_rounds + 1):
72
+ log(INFO, "")
73
+ log(INFO, "[ROUND %s]", current_round)
71
74
  cfg[Key.CURRENT_ROUND] = current_round
72
75
 
73
76
  # Fit round
@@ -79,22 +82,19 @@ class DefaultWorkflow:
79
82
  # Evaluate round
80
83
  self.evaluate_workflow(driver, context)
81
84
 
82
- # Bookkeeping
85
+ # Bookkeeping and log results
83
86
  end_time = timeit.default_timer()
84
87
  elapsed = end_time - start_time
85
- log(INFO, "FL finished in %s", elapsed)
86
-
87
- # Log results
88
88
  hist = context.history
89
- log(INFO, "app_fit: losses_distributed %s", str(hist.losses_distributed))
90
- log(
91
- INFO,
92
- "app_fit: metrics_distributed_fit %s",
93
- str(hist.metrics_distributed_fit),
94
- )
95
- log(INFO, "app_fit: metrics_distributed %s", str(hist.metrics_distributed))
96
- log(INFO, "app_fit: losses_centralized %s", str(hist.losses_centralized))
97
- log(INFO, "app_fit: metrics_centralized %s", str(hist.metrics_centralized))
89
+ log(INFO, "")
90
+ log(INFO, "[SUMMARY]")
91
+ log(INFO, "Run finished %s rounds in %.2fs", context.config.num_rounds, elapsed)
92
+ for idx, line in enumerate(io.StringIO(str(hist))):
93
+ if idx == 0:
94
+ log(INFO, "%s", line.strip("\n"))
95
+ else:
96
+ log(INFO, "\t%s", line.strip("\n"))
97
+ log(INFO, "")
98
98
 
99
99
  # Terminate the thread
100
100
  f_stop.set()
@@ -107,12 +107,11 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None:
107
107
  if not isinstance(context, LegacyContext):
108
108
  raise TypeError(f"Expect a LegacyContext, but get {type(context).__name__}.")
109
109
 
110
- log(INFO, "Initializing global parameters")
111
110
  parameters = context.strategy.initialize_parameters(
112
111
  client_manager=context.client_manager
113
112
  )
114
113
  if parameters is not None:
115
- log(INFO, "Using initial parameters provided by strategy")
114
+ log(INFO, "Using initial global parameters provided by strategy")
116
115
  paramsrecord = compat.parameters_to_parametersrecord(
117
116
  parameters, keep_input=True
118
117
  )
@@ -140,7 +139,7 @@ def default_init_params_workflow(driver: Driver, context: Context) -> None:
140
139
  context.state.parameters_records[MAIN_PARAMS_RECORD] = paramsrecord
141
140
 
142
141
  # Evaluate initial parameters
143
- log(INFO, "Evaluating initial parameters")
142
+ log(INFO, "Evaluating initial global parameters")
144
143
  parameters = compat.parametersrecord_to_parameters(paramsrecord, keep_input=True)
145
144
  res = context.strategy.evaluate(0, parameters=parameters)
146
145
  if res is not None:
@@ -186,7 +185,9 @@ def default_centralized_evaluation_workflow(_: Driver, context: Context) -> None
186
185
  )
187
186
 
188
187
 
189
- def default_fit_workflow(driver: Driver, context: Context) -> None:
188
+ def default_fit_workflow( # pylint: disable=R0914
189
+ driver: Driver, context: Context
190
+ ) -> None:
190
191
  """Execute the default workflow for a single fit round."""
191
192
  if not isinstance(context, LegacyContext):
192
193
  raise TypeError(f"Expect a LegacyContext, but get {type(context).__name__}.")
@@ -207,12 +208,11 @@ def default_fit_workflow(driver: Driver, context: Context) -> None:
207
208
  )
208
209
 
209
210
  if not client_instructions:
210
- log(INFO, "fit_round %s: no clients selected, cancel", current_round)
211
+ log(INFO, "configure_fit: no clients selected, cancel")
211
212
  return
212
213
  log(
213
- DEBUG,
214
- "fit_round %s: strategy sampled %s clients (out of %s)",
215
- current_round,
214
+ INFO,
215
+ "configure_fit: strategy sampled %s clients (out of %s)",
216
216
  len(client_instructions),
217
217
  context.client_manager.num_available(),
218
218
  )
@@ -236,14 +236,14 @@ def default_fit_workflow(driver: Driver, context: Context) -> None:
236
236
  # collect `fit` results from all clients participating in this round
237
237
  messages = list(driver.send_and_receive(out_messages))
238
238
  del out_messages
239
+ num_failures = len([msg for msg in messages if msg.has_error()])
239
240
 
240
241
  # No exception/failure handling currently
241
242
  log(
242
- DEBUG,
243
- "fit_round %s received %s results and %s failures",
244
- current_round,
245
- len(messages),
246
- 0,
243
+ INFO,
244
+ "aggregate_fit: received %s results and %s failures",
245
+ len(messages) - num_failures,
246
+ num_failures,
247
247
  )
248
248
 
249
249
  # Aggregate training results
@@ -288,12 +288,11 @@ def default_evaluate_workflow(driver: Driver, context: Context) -> None:
288
288
  client_manager=context.client_manager,
289
289
  )
290
290
  if not client_instructions:
291
- log(INFO, "evaluate_round %s: no clients selected, cancel", current_round)
291
+ log(INFO, "configure_evaluate: no clients selected, skipping evaluation")
292
292
  return
293
293
  log(
294
- DEBUG,
295
- "evaluate_round %s: strategy sampled %s clients (out of %s)",
296
- current_round,
294
+ INFO,
295
+ "configure_evaluate: strategy sampled %s clients (out of %s)",
297
296
  len(client_instructions),
298
297
  context.client_manager.num_available(),
299
298
  )
@@ -317,14 +316,14 @@ def default_evaluate_workflow(driver: Driver, context: Context) -> None:
317
316
  # collect `evaluate` results from all clients participating in this round
318
317
  messages = list(driver.send_and_receive(out_messages))
319
318
  del out_messages
319
+ num_failures = len([msg for msg in messages if msg.has_error()])
320
320
 
321
321
  # No exception/failure handling currently
322
322
  log(
323
- DEBUG,
324
- "evaluate_round %s received %s results and %s failures",
325
- current_round,
326
- len(messages),
327
- 0,
323
+ INFO,
324
+ "aggregate_evaluate: received %s results and %s failures",
325
+ len(messages) - num_failures,
326
+ num_failures,
328
327
  )
329
328
 
330
329
  # Aggregate the evaluation results
@@ -15,8 +15,10 @@
15
15
  """Secure Aggregation workflows."""
16
16
 
17
17
 
18
+ from .secagg_workflow import SecAggWorkflow
18
19
  from .secaggplus_workflow import SecAggPlusWorkflow
19
20
 
20
21
  __all__ = [
21
22
  "SecAggPlusWorkflow",
23
+ "SecAggWorkflow",
22
24
  ]
@@ -0,0 +1,112 @@
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
+ """Workflow for the SecAgg protocol."""
16
+
17
+
18
+ from typing import Optional, Union
19
+
20
+ from .secaggplus_workflow import SecAggPlusWorkflow
21
+
22
+
23
+ class SecAggWorkflow(SecAggPlusWorkflow):
24
+ """The workflow for the SecAgg protocol.
25
+
26
+ The SecAgg protocol ensures the secure summation of integer vectors owned by
27
+ multiple parties, without accessing any individual integer vector. This workflow
28
+ allows the server to compute the weighted average of model parameters across all
29
+ clients, ensuring individual contributions remain private. This is achieved by
30
+ clients sending both, a weighting factor and a weighted version of the locally
31
+ updated parameters, both of which are masked for privacy. Specifically, each
32
+ client uploads "[w, w * params]" with masks, where weighting factor 'w' is the
33
+ number of examples ('num_examples') and 'params' represents the model parameters
34
+ ('parameters') from the client's `FitRes`. The server then aggregates these
35
+ contributions to compute the weighted average of model parameters.
36
+
37
+ The protocol involves four main stages:
38
+ - 'setup': Send SecAgg configuration to clients and collect their public keys.
39
+ - 'share keys': Broadcast public keys among clients and collect encrypted secret
40
+ key shares.
41
+ - 'collect masked vectors': Forward encrypted secret key shares to target clients
42
+ and collect masked model parameters.
43
+ - 'unmask': Collect secret key shares to decrypt and aggregate the model parameters.
44
+
45
+ Only the aggregated model parameters are exposed and passed to
46
+ `Strategy.aggregate_fit`, ensuring individual data privacy.
47
+
48
+ Parameters
49
+ ----------
50
+ reconstruction_threshold : Union[int, float]
51
+ The minimum number of shares required to reconstruct a client's private key,
52
+ or, if specified as a float, it represents the proportion of the total number
53
+ of shares needed for reconstruction. This threshold ensures privacy by allowing
54
+ for the recovery of contributions from dropped clients during aggregation,
55
+ without compromising individual client data.
56
+ max_weight : Optional[float] (default: 1000.0)
57
+ The maximum value of the weight that can be assigned to any single client's
58
+ update during the weighted average calculation on the server side, e.g., in the
59
+ FedAvg algorithm.
60
+ clipping_range : float, optional (default: 8.0)
61
+ The range within which model parameters are clipped before quantization.
62
+ This parameter ensures each model parameter is bounded within
63
+ [-clipping_range, clipping_range], facilitating quantization.
64
+ quantization_range : int, optional (default: 4194304, this equals 2**22)
65
+ The size of the range into which floating-point model parameters are quantized,
66
+ mapping each parameter to an integer in [0, quantization_range-1]. This
67
+ facilitates cryptographic operations on the model updates.
68
+ modulus_range : int, optional (default: 4294967296, this equals 2**32)
69
+ The range of values from which random mask entries are uniformly sampled
70
+ ([0, modulus_range-1]). `modulus_range` must be less than 4294967296.
71
+ Please use 2**n values for `modulus_range` to prevent overflow issues.
72
+ timeout : Optional[float] (default: None)
73
+ The timeout duration in seconds. If specified, the workflow will wait for
74
+ replies for this duration each time. If `None`, there is no time limit and
75
+ the workflow will wait until replies for all messages are received.
76
+
77
+ Notes
78
+ -----
79
+ - Each client's private key is split into N shares under the SecAgg protocol, where
80
+ N is the number of selected clients.
81
+ - Generally, higher `reconstruction_threshold` means better privacy guarantees but
82
+ less tolerance to dropouts.
83
+ - Too large `max_weight` may compromise the precision of the quantization.
84
+ - `modulus_range` must be 2**n and larger than `quantization_range`.
85
+ - When `reconstruction_threshold` is a float, it is interpreted as the proportion of
86
+ the number of all selected clients needed for the reconstruction of a private key.
87
+ This feature enables flexibility in setting the security threshold relative to the
88
+ number of selected clients.
89
+ - `reconstruction_threshold`, and the quantization parameters
90
+ (`clipping_range`, `quantization_range`, `modulus_range`) play critical roles in
91
+ balancing privacy, robustness, and efficiency within the SecAgg protocol.
92
+ """
93
+
94
+ def __init__( # pylint: disable=R0913
95
+ self,
96
+ reconstruction_threshold: Union[int, float],
97
+ *,
98
+ max_weight: float = 1000.0,
99
+ clipping_range: float = 8.0,
100
+ quantization_range: int = 4194304,
101
+ modulus_range: int = 4294967296,
102
+ timeout: Optional[float] = None,
103
+ ) -> None:
104
+ super().__init__(
105
+ num_shares=1.0,
106
+ reconstruction_threshold=reconstruction_threshold,
107
+ max_weight=max_weight,
108
+ clipping_range=clipping_range,
109
+ quantization_range=quantization_range,
110
+ modulus_range=modulus_range,
111
+ timeout=timeout,
112
+ )
@@ -17,7 +17,7 @@
17
17
 
18
18
  import random
19
19
  from dataclasses import dataclass, field
20
- from logging import ERROR, WARN
20
+ from logging import DEBUG, ERROR, INFO, WARN
21
21
  from typing import Dict, List, Optional, Set, Union, cast
22
22
 
23
23
  import flwr.common.recordset_compat as compat
@@ -101,7 +101,7 @@ class SecAggPlusWorkflow:
101
101
  - 'setup': Send SecAgg+ configuration to clients and collect their public keys.
102
102
  - 'share keys': Broadcast public keys among clients and collect encrypted secret
103
103
  key shares.
104
- - 'collect masked inputs': Forward encrypted secret key shares to target clients
104
+ - 'collect masked vectors': Forward encrypted secret key shares to target clients
105
105
  and collect masked model parameters.
106
106
  - 'unmask': Collect secret key shares to decrypt and aggregate the model parameters.
107
107
 
@@ -195,12 +195,15 @@ class SecAggPlusWorkflow:
195
195
  steps = (
196
196
  self.setup_stage,
197
197
  self.share_keys_stage,
198
- self.collect_masked_input_stage,
198
+ self.collect_masked_vectors_stage,
199
199
  self.unmask_stage,
200
200
  )
201
+ log(INFO, "Secure aggregation commencing.")
201
202
  for step in steps:
202
203
  if not step(driver, context, state):
204
+ log(INFO, "Secure aggregation halted.")
203
205
  return
206
+ log(INFO, "Secure aggregation completed.")
204
207
 
205
208
  def _check_init_params(self) -> None: # pylint: disable=R0912
206
209
  # Check `num_shares`
@@ -287,6 +290,16 @@ class SecAggPlusWorkflow:
287
290
  proxy_fitins_lst = context.strategy.configure_fit(
288
291
  current_round, parameters, context.client_manager
289
292
  )
293
+ if not proxy_fitins_lst:
294
+ log(INFO, "configure_fit: no clients selected, cancel")
295
+ return False
296
+ log(
297
+ INFO,
298
+ "configure_fit: strategy sampled %s clients (out of %s)",
299
+ len(proxy_fitins_lst),
300
+ context.client_manager.num_available(),
301
+ )
302
+
290
303
  state.nid_to_fitins = {
291
304
  proxy.node_id: compat.fitins_to_recordset(fitins, False)
292
305
  for proxy, fitins in proxy_fitins_lst
@@ -362,12 +375,22 @@ class SecAggPlusWorkflow:
362
375
  ttl="",
363
376
  )
364
377
 
378
+ log(
379
+ DEBUG,
380
+ "[Stage 0] Sending configurations to %s clients.",
381
+ len(state.active_node_ids),
382
+ )
365
383
  msgs = driver.send_and_receive(
366
384
  [make(node_id) for node_id in state.active_node_ids], timeout=self.timeout
367
385
  )
368
386
  state.active_node_ids = {
369
387
  msg.metadata.src_node_id for msg in msgs if not msg.has_error()
370
388
  }
389
+ log(
390
+ DEBUG,
391
+ "[Stage 0] Received public keys from %s clients.",
392
+ len(state.active_node_ids),
393
+ )
371
394
 
372
395
  for msg in msgs:
373
396
  if msg.has_error():
@@ -401,12 +424,22 @@ class SecAggPlusWorkflow:
401
424
  )
402
425
 
403
426
  # Broadcast public keys to clients and receive secret key shares
427
+ log(
428
+ DEBUG,
429
+ "[Stage 1] Forwarding public keys to %s clients.",
430
+ len(state.active_node_ids),
431
+ )
404
432
  msgs = driver.send_and_receive(
405
433
  [make(node_id) for node_id in state.active_node_ids], timeout=self.timeout
406
434
  )
407
435
  state.active_node_ids = {
408
436
  msg.metadata.src_node_id for msg in msgs if not msg.has_error()
409
437
  }
438
+ log(
439
+ DEBUG,
440
+ "[Stage 1] Received encrypted key shares from %s clients.",
441
+ len(state.active_node_ids),
442
+ )
410
443
 
411
444
  # Build forward packet list dictionary
412
445
  srcs: List[int] = []
@@ -437,16 +470,16 @@ class SecAggPlusWorkflow:
437
470
 
438
471
  return self._check_threshold(state)
439
472
 
440
- def collect_masked_input_stage(
473
+ def collect_masked_vectors_stage(
441
474
  self, driver: Driver, context: LegacyContext, state: WorkflowState
442
475
  ) -> bool:
443
- """Execute the 'collect masked input' stage."""
476
+ """Execute the 'collect masked vectors' stage."""
444
477
  cfg = context.state.configs_records[MAIN_CONFIGS_RECORD]
445
478
 
446
- # Send secret key shares to clients (plus FitIns) and collect masked input
479
+ # Send secret key shares to clients (plus FitIns) and collect masked vectors
447
480
  def make(nid: int) -> Message:
448
481
  cfgs_dict = {
449
- Key.STAGE: Stage.COLLECT_MASKED_INPUT,
482
+ Key.STAGE: Stage.COLLECT_MASKED_VECTORS,
450
483
  Key.CIPHERTEXT_LIST: state.forward_ciphertexts[nid],
451
484
  Key.SOURCE_LIST: state.forward_srcs[nid],
452
485
  }
@@ -461,12 +494,22 @@ class SecAggPlusWorkflow:
461
494
  ttl="",
462
495
  )
463
496
 
497
+ log(
498
+ DEBUG,
499
+ "[Stage 2] Forwarding encrypted key shares to %s clients.",
500
+ len(state.active_node_ids),
501
+ )
464
502
  msgs = driver.send_and_receive(
465
503
  [make(node_id) for node_id in state.active_node_ids], timeout=self.timeout
466
504
  )
467
505
  state.active_node_ids = {
468
506
  msg.metadata.src_node_id for msg in msgs if not msg.has_error()
469
507
  }
508
+ log(
509
+ DEBUG,
510
+ "[Stage 2] Received masked vectors from %s clients.",
511
+ len(state.active_node_ids),
512
+ )
470
513
 
471
514
  # Clear cache
472
515
  del state.forward_ciphertexts, state.forward_srcs, state.nid_to_fitins
@@ -487,7 +530,7 @@ class SecAggPlusWorkflow:
487
530
 
488
531
  return self._check_threshold(state)
489
532
 
490
- def unmask_stage( # pylint: disable=R0912, R0914
533
+ def unmask_stage( # pylint: disable=R0912, R0914, R0915
491
534
  self, driver: Driver, context: LegacyContext, state: WorkflowState
492
535
  ) -> bool:
493
536
  """Execute the 'unmask' stage."""
@@ -516,12 +559,22 @@ class SecAggPlusWorkflow:
516
559
  ttl="",
517
560
  )
518
561
 
562
+ log(
563
+ DEBUG,
564
+ "[Stage 3] Requesting key shares from %s clients to remove masks.",
565
+ len(state.active_node_ids),
566
+ )
519
567
  msgs = driver.send_and_receive(
520
568
  [make(node_id) for node_id in state.active_node_ids], timeout=self.timeout
521
569
  )
522
570
  state.active_node_ids = {
523
571
  msg.metadata.src_node_id for msg in msgs if not msg.has_error()
524
572
  }
573
+ log(
574
+ DEBUG,
575
+ "[Stage 3] Received key shares from %s clients.",
576
+ len(state.active_node_ids),
577
+ )
525
578
 
526
579
  # Build collected shares dict
527
580
  collected_shares_dict: Dict[int, List[bytes]] = {}
@@ -534,7 +587,7 @@ class SecAggPlusWorkflow:
534
587
  for owner_nid, share in zip(nids, shares):
535
588
  collected_shares_dict[owner_nid].append(share)
536
589
 
537
- # Remove mask for every client who is available after collect_masked_input stage
590
+ # Remove masks for every active client after collect_masked_vectors stage
538
591
  masked_vector = state.aggregate_ndarrays
539
592
  del state.aggregate_ndarrays
540
593
  for nid, share_list in collected_shares_dict.items():
@@ -585,6 +638,15 @@ class SecAggPlusWorkflow:
585
638
  vec += offset
586
639
  vec *= inv_dq_total_ratio
587
640
  state.aggregate_ndarrays = aggregated_vector
641
+
642
+ # No exception/failure handling currently
643
+ log(
644
+ INFO,
645
+ "aggregate_fit: received %s results and %s failures",
646
+ 1,
647
+ 0,
648
+ )
649
+
588
650
  final_fitres = FitRes(
589
651
  status=Status(code=Code.OK, message=""),
590
652
  parameters=ndarrays_to_parameters(aggregated_vector),
@@ -597,5 +659,18 @@ class SecAggPlusWorkflow:
597
659
  False,
598
660
  driver.run_id, # type: ignore
599
661
  )
600
- context.strategy.aggregate_fit(current_round, [(empty_proxy, final_fitres)], [])
662
+ aggregated_result = context.strategy.aggregate_fit(
663
+ current_round, [(empty_proxy, final_fitres)], []
664
+ )
665
+ parameters_aggregated, metrics_aggregated = aggregated_result
666
+
667
+ # Update the parameters and write history
668
+ if parameters_aggregated:
669
+ paramsrecord = compat.parameters_to_parametersrecord(
670
+ parameters_aggregated, True
671
+ )
672
+ context.state.parameters_records[MAIN_PARAMS_RECORD] = paramsrecord
673
+ context.history.add_metrics_distributed_fit(
674
+ server_round=current_round, metrics=metrics_aggregated
675
+ )
601
676
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flwr-nightly
3
- Version: 1.8.0.dev20240310
3
+ Version: 1.8.0.dev20240311
4
4
  Summary: Flower: A Friendly Federated Learning Framework
5
5
  Home-page: https://flower.ai
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ flwr/cli/app.py,sha256=38thPnMydBmNAxNE9mz4By-KdRUhJfoUgeDuAxMYF_U,1095
4
4
  flwr/cli/example.py,sha256=EGPYLMQf2MgcYRn5aPp_eYYUA39M8dm69PUM4zhlHuk,2184
5
5
  flwr/cli/flower_toml.py,sha256=KSimz9EW9SkZdPnRRSj5XquQfXkCoPZHvLXvu8b4wQ4,3785
6
6
  flwr/cli/new/__init__.py,sha256=cQzK1WH4JP2awef1t2UQ2xjl1agVEz9rwutV18SWV1k,789
7
- flwr/cli/new/new.py,sha256=NSJEDkNVz6CmtOXPN06v8xdjP27xHI83db4OS3EAMLA,4396
7
+ flwr/cli/new/new.py,sha256=hEXuk7O1caO2XwfuTYvZdxZjPEovpFekX31KLYp-WRY,4522
8
8
  flwr/cli/new/templates/__init__.py,sha256=4luU8RL-CK8JJCstQ_ON809W9bNTkY1l9zSaPKBkgwY,725
9
9
  flwr/cli/new/templates/app/README.md.tpl,sha256=YcrwN6q0dkHaAW5Cual0mnny2_LBs5mQ7mQsMaRzs0I,686
10
10
  flwr/cli/new/templates/app/__init__.py,sha256=DU7QMY7IhMQyuwm_tja66xU0KXTWQFqzfTqwg-_NJdE,729
@@ -24,9 +24,9 @@ flwr/cli/new/templates/app/requirements.pytorch.txt.tpl,sha256=CiAhFk8IASOnhTbDT
24
24
  flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl,sha256=MsiO0GbUe35h5giVMaE2YykKMAhtC5ccAc_4EcmJUNs,209
25
25
  flwr/cli/run/__init__.py,sha256=oCd6HmQDx-sqver1gecgx-uMA38BLTSiiKpl7RGNceg,789
26
26
  flwr/cli/run/run.py,sha256=6iP2XW8jvB6gePLc7qGNdueG1W4Qai-rLsev-wKkPEg,3286
27
- flwr/cli/utils.py,sha256=9cCEIt8QmJXz85JNmPk1IHPd7p8E3KDn6h5CfF0nDL4,1926
27
+ flwr/cli/utils.py,sha256=_V2BlFVNNG2naZrq227fZ8o4TxBN_hB-4fQsen9uQoo,2300
28
28
  flwr/client/__init__.py,sha256=futk_IdY_N1h8BTve4Iru51bxm7H1gv58ZPIXWi5XUA,1187
29
- flwr/client/app.py,sha256=WtQDlLAtCfrBouTAV7CaMlT3EgtS9RbFMejV37oyyz8,24001
29
+ flwr/client/app.py,sha256=xCCpP-fMEFdTEaSWOP93JPIDfjRhx5Z1uI1h6YlJteo,24784
30
30
  flwr/client/client.py,sha256=Vp9UkOkoHdNfn6iMYZsj_5m_GICiFfUlKEVaLad-YhM,8183
31
31
  flwr/client/client_app.py,sha256=AzSCU8ZiBtOLgcrGEJxIU4DV4Jf7WQHxY5QxESfWo-w,8079
32
32
  flwr/client/dpfedavg_numpy_client.py,sha256=9Tnig4iml2J88HBKNahegjXjbfvIQyBtaIQaqjbeqsA,7435
@@ -37,11 +37,12 @@ flwr/client/grpc_rere_client/connection.py,sha256=3kpnUbS06rNQ969EybGx7zZfQPc2Jm
37
37
  flwr/client/message_handler/__init__.py,sha256=abHvBRJJiiaAMNgeILQbMOa6h8WqMK2BcnvxwQZFpic,719
38
38
  flwr/client/message_handler/message_handler.py,sha256=SlIU-l6GgB3wfA1Qq2x7z1SSSCXO4SO4pM122QNyTvU,6516
39
39
  flwr/client/message_handler/task_handler.py,sha256=ZDJBKmrn2grRMNl1rU1iGs7FiMHL5VmZiSp_6h9GHVU,1824
40
- flwr/client/mod/__init__.py,sha256=waY4PmRcYWmw5CeuZCsCL_o0pRH9yRIx8VzLd4QHa0w,1015
40
+ flwr/client/mod/__init__.py,sha256=OowF__U4CR0PoNSvniuLFr53RiUBVruZCiRam5uVtnk,1030
41
41
  flwr/client/mod/centraldp_mods.py,sha256=8Jiy42idsYs_gF6g5kZpjeyG98vyuSoYq9E-OibyrNk,5125
42
42
  flwr/client/mod/localdp_mod.py,sha256=fFLtqHAzWuMG5tL2O_Q4nrkqwGR6AzHkA82Avkajrdc,4492
43
- flwr/client/mod/secure_aggregation/__init__.py,sha256=AzCdezuzX2BfXUuxVRwXdv8-zUIXoU-Bf6u4LRhzvg8,796
44
- flwr/client/mod/secure_aggregation/secaggplus_mod.py,sha256=tSXJSSohf_6JODFNXEoVYzzD6rsYAHr8MJtNGPEvmuI,19922
43
+ flwr/client/mod/secure_aggregation/__init__.py,sha256=Qo2R-NqsyoP0oX73TyDfQRu9P6DCNXhgqGbhmGIBaJA,849
44
+ flwr/client/mod/secure_aggregation/secagg_mod.py,sha256=wI9tuIEvMUETz-wVIEbPYvh-1nK9CEylBLGoVpNhL94,1095
45
+ flwr/client/mod/secure_aggregation/secaggplus_mod.py,sha256=qzQiePAl3cz4MT_THiNmEfUiJoawJDiMd0B62HEYegY,19892
45
46
  flwr/client/mod/utils.py,sha256=lvETHcCYsSWz7h8I772hCV_kZspxqlMqzriMZ-SxmKc,1226
46
47
  flwr/client/node_state.py,sha256=KTTs_l4I0jBM7IsSsbAGjhfL_yZC3QANbzyvyfZBRDM,1778
47
48
  flwr/client/node_state_tests.py,sha256=gPwz0zf2iuDSa11jedkur_u3Xm7lokIDG5ALD2MCvSw,2195
@@ -59,7 +60,7 @@ flwr/common/differential_privacy_constants.py,sha256=c7b7tqgvT7yMK0XN9ndiTBs4mQf
59
60
  flwr/common/dp.py,sha256=Hc3lLHihjexbJaD_ft31gdv9XRcwOTgDBwJzICuok3A,2004
60
61
  flwr/common/exit_handlers.py,sha256=2Nt0wLhc17KQQsLPFSRAjjhUiEFfJK6tNozdGiIY4Fs,2812
61
62
  flwr/common/grpc.py,sha256=HimjpTtIY3Vfqtlq3u-CYWjqAl9rSn0uo3A8JjhUmwQ,2273
62
- flwr/common/logger.py,sha256=8874V63euuxIrdltyc2nX1XH2fs2VUvRZC7KUOPFNYk,6120
63
+ flwr/common/logger.py,sha256=Plhm9fsi4ewb90eGALQZ9xBkR0cGEsckX5RLSMEaS3M,6118
63
64
  flwr/common/message.py,sha256=Z-k3a1HgoyWBm3NS0_bphfYbJtJ8YqxfXR68AvXXkuA,9808
64
65
  flwr/common/object_ref.py,sha256=ELoUCAFO-vbjJC41CGpa-WBG2SLYe3ErW-d9YCG3zqA,4961
65
66
  flwr/common/parameter.py,sha256=-bFAUayToYDF50FZGrBC1hQYJCQDtB2bbr3ZuVLMtdE,2095
@@ -78,7 +79,7 @@ flwr/common/secure_aggregation/crypto/shamir.py,sha256=yY35ZgHlB4YyGW_buG-1X-0M-
78
79
  flwr/common/secure_aggregation/crypto/symmetric_encryption.py,sha256=-zDyQoTsHHQjR7o-92FNIikg1zM_Ke9yynaD5u2BXbQ,3546
79
80
  flwr/common/secure_aggregation/ndarrays_arithmetic.py,sha256=66mNQCz64r7qzvXwFrXP6zz7YMi8EkTOABN7KulkKc4,3026
80
81
  flwr/common/secure_aggregation/quantization.py,sha256=appui7GGrkRPsupF59TkapeV4Na_CyPi73JtJ1pimdI,2310
81
- flwr/common/secure_aggregation/secaggplus_constants.py,sha256=gzFVMSMiI4gI6MoaLKtFdQ_3iTFeDbY_bRbc9lKgYo8,2177
82
+ flwr/common/secure_aggregation/secaggplus_constants.py,sha256=Fh7-n6pgL4TUnHpNYXo8iW-n5cOGQgQa-c7RcU80tqQ,2183
82
83
  flwr/common/secure_aggregation/secaggplus_utils.py,sha256=87bNZX6CmQekj935R4u3m5hsaEkkfKtGSA-VG2c-O9w,3221
83
84
  flwr/common/serde.py,sha256=tmREmZtV3af0tnx9vp45KWGlUUqoVQyJMszkUNvVkKM,22014
84
85
  flwr/common/telemetry.py,sha256=JkFB6WBOskqAJfzSM-l6tQfRiSi2oiysClfg0-5T7NY,7782
@@ -178,7 +179,7 @@ flwr/server/superlink/fleet/vce/backend/backend.py,sha256=LJsKl7oixVvptcG98Rd9ej
178
179
  flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=BYgzVH1uz8nk6mOP6GhgSxjrdCe7xtkzb7nhPbKStFM,6317
179
180
  flwr/server/superlink/fleet/vce/vce_api.py,sha256=Yq4i9fduafnoWSHCLn0mmkCTS9oZqwycH8gbKa4bPXo,11168
180
181
  flwr/server/superlink/state/__init__.py,sha256=ij-7Ms-hyordQdRmGQxY1-nVa4OhixJ0jr7_YDkys0s,1003
181
- flwr/server/superlink/state/in_memory_state.py,sha256=sZX5XcpnU9cafhhC4Or5oRGIbKR2AKdjIfBDtMVGNLQ,8105
182
+ flwr/server/superlink/state/in_memory_state.py,sha256=-dEhGm_y5zdamyRr57FrEJDgnNko2dytyqc8vW_q7vw,8265
182
183
  flwr/server/superlink/state/sqlite_state.py,sha256=Adc2g1DecAN9Cl9F8lekuTb885mIHiOi6sQv4nxbmSc,21203
183
184
  flwr/server/superlink/state/state.py,sha256=JtsI92HfdKd8KzBQ9Om7A7xwngDXVxtET2Bk9aQ7nao,5316
184
185
  flwr/server/superlink/state/state_factory.py,sha256=91cSB-KOAFM37z7T098WxTkVeKNaAZ_mTI75snn2_tk,1654
@@ -186,11 +187,12 @@ flwr/server/typing.py,sha256=2zSG-KuDAgwFPuzgVjTLDaEqJ8gXXGqFR2RD-qIk730,913
186
187
  flwr/server/utils/__init__.py,sha256=RQVbo-bcsVtp_lJBf7dL5w01FbLrr7v3YedeGp5_YMs,908
187
188
  flwr/server/utils/tensorboard.py,sha256=k0G6bqsLx7wfYbH2KtXsDYcOCfyIeE12-hefXA7lZdg,5485
188
189
  flwr/server/utils/validator.py,sha256=IJN2475yyD_i_9kg_SJ_JodIuZh58ufpWGUDQRAqu2s,4740
189
- flwr/server/workflow/__init__.py,sha256=VegVFwlbyNSVk8MUTzYdSKb9u0HFKEABwnT2-rAHUmg,864
190
+ flwr/server/workflow/__init__.py,sha256=SXY0XkwbkezFBxxrFB5hKUtmtAgnYISBkPouR1V71ss,902
190
191
  flwr/server/workflow/constant.py,sha256=q4DLdR8Krlxuewq2AQjwTL75hphxE5ODNz4AhViHMXk,1082
191
- flwr/server/workflow/default_workflows.py,sha256=tLcsjwngOUv9UEVVr-hFAmGCIqKEGZ9lKgEVRzjiyOM,12601
192
- flwr/server/workflow/secure_aggregation/__init__.py,sha256=vvhOLJNIJkMPwnWR2HqQOKHfAa4bAfZLUqgXadGPOnc,814
193
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=zayq85mvJ45ySm9Zalk1GJms9OpYhkNDxUyUADifdmM,26517
192
+ flwr/server/workflow/default_workflows.py,sha256=fOGQiY0LrRPOWLjr3zlZoEapts3dgg4XdHravb4-shE,12601
193
+ flwr/server/workflow/secure_aggregation/__init__.py,sha256=3XlgDOjD_hcukTGl6Bc1B-8M_dPlVSJuTbvXIbiO-Ic,880
194
+ flwr/server/workflow/secure_aggregation/secagg_workflow.py,sha256=wpAkYPId0nfK6SgpUAtsCni4_MQLd-uqJ81tUKu3xlI,5838
195
+ flwr/server/workflow/secure_aggregation/secaggplus_workflow.py,sha256=9LxvdTgaJ9AG3E3z30a-Z556wiR9kXNljEUzW-GjHsE,28954
194
196
  flwr/simulation/__init__.py,sha256=hpoKzdovrH0_Cf8HIcXxQxyUUb3BiSk-WUNLf5STHcc,1400
195
197
  flwr/simulation/app.py,sha256=WqJxdXTEuehwMW605p5NMmvBbKYx5tuqnV3Mp7jSWXM,13904
196
198
  flwr/simulation/ray_transport/__init__.py,sha256=FsaAnzC4cw4DqoouBCix6496k29jACkfeIam55BvW9g,734
@@ -198,8 +200,8 @@ flwr/simulation/ray_transport/ray_actor.py,sha256=zRETW_xuCAOLRFaYnQ-q3IBSz0LIv_
198
200
  flwr/simulation/ray_transport/ray_client_proxy.py,sha256=L49gtigsf4vTQgRiqzOgcPEuS_l-EuTj29Ohw6ekbSI,6721
199
201
  flwr/simulation/ray_transport/utils.py,sha256=TYdtfg1P9VfTdLMOJlifInGpxWHYs9UfUqIv2wfkRLA,2392
200
202
  flwr/simulation/run_simulation.py,sha256=1JMP5nYFeGrZzcpw_Q0aDvyla2AvRz5aFJw1i1InSvs,15681
201
- flwr_nightly-1.8.0.dev20240310.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
202
- flwr_nightly-1.8.0.dev20240310.dist-info/METADATA,sha256=d5nH-Dd4FCJdSkN8l3e8hGYPG4sKASIN6VeqrnmEZ_g,15257
203
- flwr_nightly-1.8.0.dev20240310.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
204
- flwr_nightly-1.8.0.dev20240310.dist-info/entry_points.txt,sha256=utu2wybGyYJSTtsB2ktY_gmy-XtMFo9EFZdishX0zR4,320
205
- flwr_nightly-1.8.0.dev20240310.dist-info/RECORD,,
203
+ flwr_nightly-1.8.0.dev20240311.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
204
+ flwr_nightly-1.8.0.dev20240311.dist-info/METADATA,sha256=BCxj-WJIOnMV5R2uqtLoBFk1KLRmgsLjScmSY0YPscI,15257
205
+ flwr_nightly-1.8.0.dev20240311.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
206
+ flwr_nightly-1.8.0.dev20240311.dist-info/entry_points.txt,sha256=utu2wybGyYJSTtsB2ktY_gmy-XtMFo9EFZdishX0zR4,320
207
+ flwr_nightly-1.8.0.dev20240311.dist-info/RECORD,,