Skip to content

Commit

Permalink
Merge branch 'main' into doc/update-supernode-docker-image
Browse files Browse the repository at this point in the history
  • Loading branch information
tanertopal authored Aug 21, 2024
2 parents d38aca1 + 53176af commit 0fb944f
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 74 deletions.
9 changes: 4 additions & 5 deletions src/py/flwr/cli/run/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
),
Expand Down Expand Up @@ -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"))
),
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions src/py/flwr/client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 17 additions & 4 deletions src/py/flwr/client/node_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/py/flwr/client/supernode/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
)


Expand Down
4 changes: 4 additions & 0 deletions src/py/flwr/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -67,6 +69,7 @@
"Code",
"Config",
"ConfigsRecord",
"ConfigsRecordValues",
"Context",
"DEFAULT_TTL",
"DisconnectRes",
Expand All @@ -88,6 +91,7 @@
"Metrics",
"MetricsAggregationFn",
"MetricsRecord",
"MetricsRecordValues",
"NDArray",
"NDArrays",
"Parameters",
Expand Down
32 changes: 24 additions & 8 deletions src/py/flwr/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
"""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

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

Expand Down Expand Up @@ -104,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.
Expand Down Expand Up @@ -165,26 +178,29 @@ 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 = {}

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
Expand Down
2 changes: 1 addition & 1 deletion src/py/flwr/common/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 49 additions & 15 deletions src/py/flwr/common/record/configsrecord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <flwr.common.RecordSet.html#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()):
Expand Down
68 changes: 54 additions & 14 deletions src/py/flwr/common/record/metricsrecord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <flwr.common.RecordSet.html#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()):
Expand Down
Loading

0 comments on commit 0fb944f

Please sign in to comment.