From 063220ced0e7725ecc33036bca41c2d5e74ec034 Mon Sep 17 00:00:00 2001 From: Charles Beauville Date: Wed, 21 Aug 2024 16:44:06 +0200 Subject: [PATCH 1/5] break(framework) Use spaces instead of commas for separating config args (#4000) Co-authored-by: Javier --- src/py/flwr/cli/run/run.py | 9 ++++----- src/py/flwr/client/supernode/app.py | 4 ++-- src/py/flwr/common/config.py | 18 +++++++++++------- src/py/flwr/common/config_test.py | 2 +- src/py/flwr/superexec/app.py | 6 +++--- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/py/flwr/cli/run/run.py b/src/py/flwr/cli/run/run.py index 2df14969e24e..ff7aeac226b2 100644 --- a/src/py/flwr/cli/run/run.py +++ b/src/py/flwr/cli/run/run.py @@ -56,7 +56,8 @@ def run( "--run-config", "-c", help="Override configuration key-value pairs, should be of the format:\n\n" - "`--run-config key1=value1,key2=value2 --run-config key3=value3`\n\n" + '`--run-config \'key1="value1" key2="value2"\' ' + "--run-config 'key3=\"value3\"'`\n\n" "Note that `key1`, `key2`, and `key3` in this example need to exist " "inside the `pyproject.toml` in order to be properly overriden.", ), @@ -171,9 +172,7 @@ def _run_with_superexec( req = StartRunRequest( fab=fab_to_proto(fab), - override_config=user_config_to_proto( - parse_config_args(config_overrides, separator=",") - ), + override_config=user_config_to_proto(parse_config_args(config_overrides)), federation_config=user_config_to_proto( flatten_dict(federation_config.get("options")) ), @@ -214,7 +213,7 @@ def _run_without_superexec( ] if config_overrides: - command.extend(["--run-config", f"{','.join(config_overrides)}"]) + command.extend(["--run-config", f"{' '.join(config_overrides)}"]) # Run the simulation subprocess.run( diff --git a/src/py/flwr/client/supernode/app.py b/src/py/flwr/client/supernode/app.py index 2e6b942b5d2f..1b027d534a50 100644 --- a/src/py/flwr/client/supernode/app.py +++ b/src/py/flwr/client/supernode/app.py @@ -325,9 +325,9 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--node-config", type=str, - help="A comma separated list of key/value pairs (separated by `=`) to " + help="A space separated list of key/value pairs (separated by `=`) to " "configure the SuperNode. " - "E.g. --node-config 'key1=\"value1\",partition-id=0,num-partitions=100'", + "E.g. --node-config 'key1=\"value1\" partition-id=0 num-partitions=100'", ) diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index a319b3cdc704..c9bf5f31b83d 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -15,6 +15,7 @@ """Provide functions for managing global Flower config.""" import os +import re from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union, cast, get_args @@ -165,7 +166,6 @@ def unflatten_dict(flat_dict: Dict[str, Any]) -> Dict[str, Any]: def parse_config_args( config: Optional[List[str]], - separator: str = ",", ) -> UserConfig: """Parse separator separated list of key-value pairs separated by '='.""" overrides: UserConfig = {} @@ -173,18 +173,22 @@ def parse_config_args( if config is None: return overrides + # Regular expression to capture key-value pairs with possible quoted values + pattern = re.compile(r"(\S+?)=(\'[^\']*\'|\"[^\"]*\"|\S+)") + for config_line in config: if config_line: - overrides_list = config_line.split(separator) + matches = pattern.findall(config_line) + if ( - len(overrides_list) == 1 - and "=" not in overrides_list - and overrides_list[0].endswith(".toml") + len(matches) == 1 + and "=" not in matches[0][0] + and matches[0][0].endswith(".toml") ): - with Path(overrides_list[0]).open("rb") as config_file: + with Path(matches[0][0]).open("rb") as config_file: overrides = flatten_dict(tomli.load(config_file)) else: - toml_str = "\n".join(overrides_list) + toml_str = "\n".join(f"{k} = {v}" for k, v in matches) overrides.update(tomli.loads(toml_str)) return overrides diff --git a/src/py/flwr/common/config_test.py b/src/py/flwr/common/config_test.py index 071263ed8531..712e07264d3f 100644 --- a/src/py/flwr/common/config_test.py +++ b/src/py/flwr/common/config_test.py @@ -245,7 +245,7 @@ def test_parse_config_args_none() -> None: def test_parse_config_args_overrides() -> None: """Test parse_config_args with key-value pairs.""" assert parse_config_args( - ["key1='value1',key2='value2'", "key3=1", "key4=2.0,key5=true,key6='value6'"] + ["key1='value1' key2='value2'", "key3=1", "key4=2.0 key5=true key6='value6'"] ) == { "key1": "value1", "key2": "value2", diff --git a/src/py/flwr/superexec/app.py b/src/py/flwr/superexec/app.py index 2ad5f12d227f..9510479ec8e1 100644 --- a/src/py/flwr/superexec/app.py +++ b/src/py/flwr/superexec/app.py @@ -93,9 +93,9 @@ def _parse_args_run_superexec() -> argparse.ArgumentParser: ) parser.add_argument( "--executor-config", - help="Key-value pairs for the executor config, separated by commas. " - 'For example:\n\n`--executor-config superlink="superlink:9091",' - 'root-certificates="certificates/superlink-ca.crt"`', + help="Key-value pairs for the executor config, separated by spaces. " + 'For example:\n\n`--executor-config \'superlink="superlink:9091" ' + 'root-certificates="certificates/superlink-ca.crt"\'`', ) parser.add_argument( "--insecure", From 0e71988460b5d9dcc4f1a4dc5eddccab3e0499c5 Mon Sep 17 00:00:00 2001 From: Heng Pan Date: Wed, 21 Aug 2024 15:56:15 +0100 Subject: [PATCH 2/5] refactor(framework:skip) Pass `sys.path` to ray processes (#3985) Co-authored-by: Javier --- .../flwr/server/superlink/fleet/vce/backend/raybackend.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py index 2087a88360c4..0ef2ef737a8f 100644 --- a/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py +++ b/src/py/flwr/server/superlink/fleet/vce/backend/raybackend.py @@ -14,6 +14,7 @@ # ============================================================================== """Ray backend for the Fleet API using the Simulation Engine.""" +import sys from logging import DEBUG, ERROR from typing import Callable, Dict, Tuple, Union @@ -111,8 +112,10 @@ def init_ray(self, backend_config: BackendConfig) -> None: if backend_config.get(self.init_args_key): for k, v in backend_config[self.init_args_key].items(): ray_init_args[k] = v - - ray.init(**ray_init_args) + ray.init( + runtime_env={"env_vars": {"PYTHONPATH": ":".join(sys.path)}}, + **ray_init_args, + ) @property def num_workers(self) -> int: From b4419ed434cfc30a2bbc95d67191db502aa29e52 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 16:53:51 +0100 Subject: [PATCH 3/5] fix(framework:skip) Enable `SuperNode` to complete context registration when `FAB` is not installed (#4049) --- src/py/flwr/client/app.py | 6 ++++-- src/py/flwr/client/node_state.py | 21 +++++++++++++++++---- src/py/flwr/common/config.py | 14 +++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/py/flwr/client/app.py b/src/py/flwr/client/app.py index 1aed5d5241dd..fb4855a09817 100644 --- a/src/py/flwr/client/app.py +++ b/src/py/flwr/client/app.py @@ -452,12 +452,14 @@ def _on_backoff(retry_state: RetryState) -> None: # Register context for this run node_state.register_context( - run_id=run_id, run=run, flwr_path=flwr_path + run_id=run_id, + run=run, + flwr_path=flwr_path, + fab=fab, ) # Retrieve context for this run context = node_state.retrieve_context(run_id=run_id) - # Create an error reply message that will never be used to prevent # the used-before-assignment linting error reply_message = message.create_error_reply( diff --git a/src/py/flwr/client/node_state.py b/src/py/flwr/client/node_state.py index 3320c90cb8cc..e16d7e34715d 100644 --- a/src/py/flwr/client/node_state.py +++ b/src/py/flwr/client/node_state.py @@ -20,8 +20,12 @@ from typing import Dict, Optional from flwr.common import Context, RecordSet -from flwr.common.config import get_fused_config, get_fused_config_from_dir -from flwr.common.typing import Run, UserConfig +from flwr.common.config import ( + get_fused_config, + get_fused_config_from_dir, + get_fused_config_from_fab, +) +from flwr.common.typing import Fab, Run, UserConfig @dataclass() @@ -44,12 +48,14 @@ def __init__( self.node_config = node_config self.run_infos: Dict[int, RunInfo] = {} + # pylint: disable=too-many-arguments def register_context( self, run_id: int, run: Optional[Run] = None, flwr_path: Optional[Path] = None, app_dir: Optional[str] = None, + fab: Optional[Fab] = None, ) -> None: """Register new run context for this node.""" if run_id not in self.run_infos: @@ -65,8 +71,15 @@ def register_context( else: raise ValueError("The specified `app_dir` must be a directory.") else: - # Load from .fab - initial_run_config = get_fused_config(run, flwr_path) if run else {} + if run: + if fab: + # Load pyproject.toml from FAB file and fuse + initial_run_config = get_fused_config_from_fab(fab.content, run) + else: + # Load pyproject.toml from installed FAB and fuse + initial_run_config = get_fused_config(run, flwr_path) + else: + initial_run_config = {} self.run_infos[run_id] = RunInfo( initial_run_config=initial_run_config, context=Context( diff --git a/src/py/flwr/common/config.py b/src/py/flwr/common/config.py index c9bf5f31b83d..eec7cfb726b7 100644 --- a/src/py/flwr/common/config.py +++ b/src/py/flwr/common/config.py @@ -21,7 +21,7 @@ import tomli -from flwr.cli.config_utils import validate_fields +from flwr.cli.config_utils import get_fab_config, validate_fields from flwr.common.constant import APP_DIR, FAB_CONFIG_FILE, FLWR_HOME from flwr.common.typing import Run, UserConfig, UserConfigValue @@ -105,6 +105,18 @@ def get_fused_config_from_dir( return fuse_dicts(flat_default_config, override_config) +def get_fused_config_from_fab(fab_file: Union[Path, bytes], run: Run) -> UserConfig: + """Fuse default config in a `FAB` with overrides in a `Run`. + + This enables obtaining a run-config without having to install the FAB. This + function mirrors `get_fused_config_from_dir`. This is useful when the execution + of the FAB is delegated to a different process. + """ + default_config = get_fab_config(fab_file)["tool"]["flwr"]["app"].get("config", {}) + flat_config_flat = flatten_dict(default_config) + return fuse_dicts(flat_config_flat, run.override_config) + + def get_fused_config(run: Run, flwr_dir: Optional[Path]) -> UserConfig: """Merge the overrides from a `Run` with the config from a FAB. From 29827fcd088cf249f47651fa05ebe5ae5122a1ac Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 19:00:30 +0100 Subject: [PATCH 4/5] feat(framework:skip) Export `MetricsRecordValues` and `ConfigsRecordValues` (#4052) --- src/py/flwr/common/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/py/flwr/common/__init__.py b/src/py/flwr/common/__init__.py index bbdf48425e0a..925f21ddb491 100644 --- a/src/py/flwr/common/__init__.py +++ b/src/py/flwr/common/__init__.py @@ -41,6 +41,7 @@ from .typing import ClientMessage as ClientMessage from .typing import Code as Code from .typing import Config as Config +from .typing import ConfigsRecordValues as ConfigsRecordValues from .typing import DisconnectRes as DisconnectRes from .typing import EvaluateIns as EvaluateIns from .typing import EvaluateRes as EvaluateRes @@ -52,6 +53,7 @@ from .typing import GetPropertiesRes as GetPropertiesRes from .typing import Metrics as Metrics from .typing import MetricsAggregationFn as MetricsAggregationFn +from .typing import MetricsRecordValues as MetricsRecordValues from .typing import NDArray as NDArray from .typing import NDArrays as NDArrays from .typing import Parameters as Parameters @@ -67,6 +69,7 @@ "Code", "Config", "ConfigsRecord", + "ConfigsRecordValues", "Context", "DEFAULT_TTL", "DisconnectRes", @@ -88,6 +91,7 @@ "Metrics", "MetricsAggregationFn", "MetricsRecord", + "MetricsRecordValues", "NDArray", "NDArrays", "Parameters", From 53176af390f086c32bfce298ef9874800b112703 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 21 Aug 2024 19:13:37 +0100 Subject: [PATCH 5/5] docs(framework) Add examples to records docstrings (#4021) Co-authored-by: Heng Pan --- src/py/flwr/common/record/configsrecord.py | 64 ++++++++--- src/py/flwr/common/record/metricsrecord.py | 68 +++++++++--- src/py/flwr/common/record/parametersrecord.py | 101 +++++++++++++++--- src/py/flwr/common/record/recordset.py | 72 ++++++++++++- 4 files changed, 258 insertions(+), 47 deletions(-) diff --git a/src/py/flwr/common/record/configsrecord.py b/src/py/flwr/common/record/configsrecord.py index 471c85f0b961..aeb311089bcd 100644 --- a/src/py/flwr/common/record/configsrecord.py +++ b/src/py/flwr/common/record/configsrecord.py @@ -58,27 +58,61 @@ def is_valid(__v: ConfigsScalar) -> None: class ConfigsRecord(TypedDict[str, ConfigsRecordValues]): - """Configs record.""" + """Configs record. + + A :code:`ConfigsRecord` is a Python dictionary designed to ensure that + each key-value pair adheres to specified data types. A :code:`ConfigsRecord` + is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + configs_dict : Optional[Dict[str, ConfigsRecordValues]] + A dictionary that stores basic types (i.e. `str`, `int`, `float`, `bytes` as + defined in `ConfigsScalar`) and lists of such types (see + `ConfigsScalarList`). + keep_input : bool (default: True) + A boolean indicating whether config passed should be deleted from the input + dictionary immediately after adding them to the record. When set + to True, the data is duplicated in memory. If memory is a concern, set + it to False. + + Examples + -------- + The usage of a :code:`ConfigsRecord` is envisioned for sending configuration values + telling the target node how to perform a certain action (e.g. train/evaluate a model + ). You can use standard Python built-in types such as :code:`float`, :code:`str` + , :code:`bytes`. All types allowed are defined in + :code:`flwr.common.ConfigsRecordValues`. While lists are supported, we + encourage you to use a :code:`ParametersRecord` instead if these are of high + dimensionality. + + Let's see some examples of how to construct a :code:`ConfigsRecord` from scratch: + + >>> from flwr.common import ConfigsRecord + >>> + >>> # A `ConfigsRecord` is a specialized Python dictionary + >>> record = ConfigsRecord({"lr": 0.1, "batch-size": 128}) + >>> # You can add more content to an existing record + >>> record["compute-average"] = True + >>> # It also supports lists + >>> record["loss-fn-coefficients"] = [0.4, 0.25, 0.35] + >>> # And string values (among other types) + >>> record["path-to-S3"] = "s3://bucket_name/folder1/fileA.json" + + Just like the other types of records in a :code:`flwr.common.RecordSet`, types are + enforced. If you need to add a custom data structure or object, we recommend to + serialise it into bytes and save it as such (bytes are allowed in a + :code:`ConfigsRecord`) + """ def __init__( self, configs_dict: Optional[Dict[str, ConfigsRecordValues]] = None, keep_input: bool = True, ) -> None: - """Construct a ConfigsRecord object. - - Parameters - ---------- - configs_dict : Optional[Dict[str, ConfigsRecordValues]] - A dictionary that stores basic types (i.e. `str`, `int`, `float`, `bytes` as - defined in `ConfigsScalar`) and lists of such types (see - `ConfigsScalarList`). - keep_input : bool (default: True) - A boolean indicating whether config passed should be deleted from the input - dictionary immediately after adding them to the record. When set - to True, the data is duplicated in memory. If memory is a concern, set - it to False. - """ + super().__init__(_check_key, _check_value) if configs_dict: for k in list(configs_dict.keys()): diff --git a/src/py/flwr/common/record/metricsrecord.py b/src/py/flwr/common/record/metricsrecord.py index 2b6e584be390..868ed82e79ca 100644 --- a/src/py/flwr/common/record/metricsrecord.py +++ b/src/py/flwr/common/record/metricsrecord.py @@ -58,26 +58,66 @@ def is_valid(__v: MetricsScalar) -> None: class MetricsRecord(TypedDict[str, MetricsRecordValues]): - """Metrics record.""" + """Metrics recod. + + A :code:`MetricsRecord` is a Python dictionary designed to ensure that + each key-value pair adheres to specified data types. A :code:`MetricsRecord` + is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + metrics_dict : Optional[Dict[str, MetricsRecordValues]] + A dictionary that stores basic types (i.e. `int`, `float` as defined + in `MetricsScalar`) and list of such types (see `MetricsScalarList`). + keep_input : bool (default: True) + A boolean indicating whether metrics should be deleted from the input + dictionary immediately after adding them to the record. When set + to True, the data is duplicated in memory. If memory is a concern, set + it to False. + + Examples + -------- + The usage of a :code:`MetricsRecord` is envisioned for communicating results + obtained when a node performs an action. A few typical examples include: + communicating the training accuracy after a model is trained locally by a + :code:`ClientApp`, reporting the validation loss obtained at a :code:`ClientApp`, + or, more generally, the output of executing a query by the :code:`ClientApp`. + Common to these examples is that the output can be typically represented by + a single scalar (:code:`int`, :code:`float`) or list of scalars. + + Let's see some examples of how to construct a :code:`MetricsRecord` from scratch: + + >>> from flwr.common import MetricsRecord + >>> + >>> # A `MetricsRecord` is a specialized Python dictionary + >>> record = MetricsRecord({"accuracy": 0.94}) + >>> # You can add more content to an existing record + >>> record["loss"] = 0.01 + >>> # It also supports lists + >>> record["loss-historic"] = [0.9, 0.5, 0.01] + + Since types are enforced, the types of the objects inserted are checked. For a + :code:`MetricsRecord`, value types allowed are those in defined in + :code:`flwr.common.MetricsRecordValues`. Similarly, only :code:`str` keys are + allowed. + + >>> from flwr.common import MetricsRecord + >>> + >>> record = MetricsRecord() # an empty record + >>> # Add unsupported value + >>> record["something-unsupported"] = {'a': 123} # Will throw a `TypeError` + + If you need a more versatily type of record try :code:`ConfigsRecord` or + :code:`ParametersRecord`. + """ def __init__( self, metrics_dict: Optional[Dict[str, MetricsRecordValues]] = None, keep_input: bool = True, ): - """Construct a MetricsRecord object. - - Parameters - ---------- - metrics_dict : Optional[Dict[str, MetricsRecordValues]] - A dictionary that stores basic types (i.e. `int`, `float` as defined - in `MetricsScalar`) and list of such types (see `MetricsScalarList`). - keep_input : bool (default: True) - A boolean indicating whether metrics should be deleted from the input - dictionary immediately after adding them to the record. When set - to True, the data is duplicated in memory. If memory is a concern, set - it to False. - """ super().__init__(_check_key, _check_value) if metrics_dict: for k in list(metrics_dict.keys()): diff --git a/src/py/flwr/common/record/parametersrecord.py b/src/py/flwr/common/record/parametersrecord.py index 93db6d387b53..f088d682497b 100644 --- a/src/py/flwr/common/record/parametersrecord.py +++ b/src/py/flwr/common/record/parametersrecord.py @@ -83,11 +83,93 @@ def _check_value(value: Array) -> None: class ParametersRecord(TypedDict[str, Array]): - """Parameters record. + r"""Parameters record. A dataclass storing named Arrays in order. This means that it holds entries as an OrderedDict[str, Array]. ParametersRecord objects can be viewed as an equivalent to - PyTorch's state_dict, but holding serialised tensors instead. + PyTorch's state_dict, but holding serialised tensors instead. A + :code:`ParametersRecord` is one of the types of records that a + `flwr.common.RecordSet `_ supports and + can therefore be used to construct :code:`common.Message` objects. + + Parameters + ---------- + array_dict : Optional[OrderedDict[str, Array]] + A dictionary that stores serialized array-like or tensor-like objects. + keep_input : bool (default: False) + A boolean indicating whether parameters should be deleted from the input + dictionary immediately after adding them to the record. If False, the + dictionary passed to `set_parameters()` will be empty once exiting from that + function. This is the desired behaviour when working with very large + models/tensors/arrays. However, if you plan to continue working with your + parameters after adding it to the record, set this flag to True. When set + to True, the data is duplicated in memory. + + Examples + -------- + The usage of :code:`ParametersRecord` is envisioned for storing data arrays (e.g. + parameters of a machine learning model). These first need to be serialized into + a :code:`flwr.common.Array` data structure. + + Let's see some examples: + + >>> import numpy as np + >>> from flwr.common import ParametersRecord + >>> from flwr.common import array_from_numpy + >>> + >>> # Let's create a simple NumPy array + >>> arr_np = np.random.randn(3, 3) + >>> + >>> # If we print it + >>> array([[-1.84242409, -1.01539537, -0.46528405], + >>> [ 0.32991896, 0.55540414, 0.44085534], + >>> [-0.10758364, 1.97619858, -0.37120501]]) + >>> + >>> # Let's create an Array out of it + >>> arr = array_from_numpy(arr_np) + >>> + >>> # If we print it you'll see (note the binary data) + >>> Array(dtype='float64', shape=[3,3], stype='numpy.ndarray', data=b'@\x99\x18...') + >>> + >>> # Adding it to a ParametersRecord: + >>> p_record = ParametersRecord({"my_array": arr}) + + Now that the NumPy array is embedded into a :code:`ParametersRecord` it could be + sent if added as part of a :code:`common.Message` or it could be saved as a + persistent state of a :code:`ClientApp` via its context. Regardless of the usecase, + we will sooner or later want to recover the array in its original NumPy + representation. For the example above, where the array was serialized using the + built-in utility function, deserialization can be done as follows: + + >>> # Use the Array's built-in method + >>> arr_np_d = arr.numpy() + >>> + >>> # If printed, it will show the exact same data as above: + >>> array([[-1.84242409, -1.01539537, -0.46528405], + >>> [ 0.32991896, 0.55540414, 0.44085534], + >>> [-0.10758364, 1.97619858, -0.37120501]]) + + If you need finer control on how your arrays are serialized and deserialized, you + can construct :code:`Array` objects directly like this: + + >>> from flwr.common import Array + >>> # Serialize your array and construct Array object + >>> arr = Array( + >>> data=ndarray.tobytes(), + >>> dtype=str(ndarray.dtype), + >>> stype="", # Could be used in a deserialization function + >>> shape=list(ndarray.shape), + >>> ) + >>> + >>> # Then you can deserialize it like this + >>> arr_np_d = np.frombuffer( + >>> buffer=array.data, + >>> dtype=array.dtype, + >>> ).reshape(array.shape) + + Note that different arrays (e.g. from PyTorch, Tensorflow) might require different + serialization mechanism. Howerver, they often support a conversion to NumPy, + therefore allowing to use the same or similar steps as in the example above. """ def __init__( @@ -95,21 +177,6 @@ def __init__( array_dict: Optional[OrderedDict[str, Array]] = None, keep_input: bool = False, ) -> None: - """Construct a ParametersRecord object. - - Parameters - ---------- - array_dict : Optional[OrderedDict[str, Array]] - A dictionary that stores serialized array-like or tensor-like objects. - keep_input : bool (default: False) - A boolean indicating whether parameters should be deleted from the input - dictionary immediately after adding them to the record. If False, the - dictionary passed to `set_parameters()` will be empty once exiting from that - function. This is the desired behaviour when working with very large - models/tensors/arrays. However, if you plan to continue working with your - parameters after adding it to the record, set this flag to True. When set - to True, the data is duplicated in memory. - """ super().__init__(_check_key, _check_value) if array_dict: for k in list(array_dict.keys()): diff --git a/src/py/flwr/common/record/recordset.py b/src/py/flwr/common/record/recordset.py index 098b73b2d429..f16a22695d6e 100644 --- a/src/py/flwr/common/record/recordset.py +++ b/src/py/flwr/common/record/recordset.py @@ -86,7 +86,77 @@ def _check_fn_configs(self, record: ConfigsRecord) -> None: class RecordSet: - """RecordSet stores groups of parameters, metrics and configs.""" + """RecordSet stores groups of parameters, metrics and configs. + + A :code:`RecordSet` is the unified mechanism by which parameters, + metrics and configs can be either stored as part of a + `flwr.common.Context `_ in your apps + or communicated as part of a + `flwr.common.Message `_ between your apps. + + Parameters + ---------- + parameters_records : Optional[Dict[str, ParametersRecord]] + A dictionary of :code:`ParametersRecords` that can be used to record + and communicate model parameters and high-dimensional arrays. + metrics_records : Optional[Dict[str, MetricsRecord]] + A dictionary of :code:`MetricsRecord` that can be used to record + and communicate scalar-valued metrics that are the result of performing + and action, for example, by a :code:`ClientApp`. + configs_records : Optional[Dict[str, ConfigsRecord]] + A dictionary of :code:`ConfigsRecord` that can be used to record + and communicate configuration values to an entity (e.g. to a + :code:`ClientApp`) + for it to adjust how an action is performed. + + Examples + -------- + A :code:`RecordSet` can hold three types of records, each designed + with an specific purpose. What is common to all of them is that they + are Python dictionaries designed to ensure that each key-value pair + adheres to specified data types. + + Let's see an example. + + >>> from flwr.common import RecordSet + >>> from flwr.common import ConfigsRecords, MetricsRecords, ParametersRecord + >>> + >>> # Let's begin with an empty record + >>> my_recordset = RecordSet() + >>> + >>> # We can create a ConfigsRecord + >>> c_record = ConfigsRecord({"lr": 0.1, "batch-size": 128}) + >>> # Adding it to the record_set would look like this + >>> my_recordset.configs_records["my_config"] = c_record + >>> + >>> # We can create a MetricsRecord following a similar process + >>> m_record = MetricsRecord({"accuracy": 0.93, "losses": [0.23, 0.1]}) + >>> # Adding it to the record_set would look like this + >>> my_recordset.metrics_records["my_metrics"] = m_record + + Adding a :code:`ParametersRecord` follows the same steps as above but first, + the array needs to be serialized and represented as a :code:`flwr.common.Array`. + If the array is a :code:`NumPy` array, you can use the built-in utility function + `array_from_numpy `_. It is often possible to + convert an array first to :code:`NumPy` and then use the aforementioned function. + + >>> from flwr.common import array_from_numpy + >>> # Creating a ParametersRecord would look like this + >>> arr_np = np.random.randn(3, 3) + >>> + >>> # You can use the built-in tool to serialize the array + >>> arr = array_from_numpy(arr_np) + >>> + >>> # Finally, create the record + >>> p_record = ParametersRecord({"my_array": arr}) + >>> + >>> # Adding it to the record_set would look like this + >>> my_recordset.configs_records["my_config"] = c_record + + For additional examples on how to construct each of the records types shown + above, please refer to the documentation for :code:`ConfigsRecord`, + :code:`MetricsRecord` and :code:`ParametersRecord`. + """ def __init__( self,