flwr-nightly 1.8.0.dev20240310__py3-none-any.whl → 1.8.0.dev20240311__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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,,