flwr-nightly 1.13.0.dev20241021__py3-none-any.whl → 1.13.0.dev20241111__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.
Potentially problematic release.
This version of flwr-nightly might be problematic. Click here for more details.
- flwr/cli/build.py +2 -2
- flwr/cli/config_utils.py +97 -0
- flwr/cli/log.py +63 -97
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -0
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/run/run.py +34 -88
- flwr/client/app.py +23 -20
- flwr/client/clientapp/app.py +22 -18
- flwr/client/nodestate/__init__.py +25 -0
- flwr/client/nodestate/in_memory_nodestate.py +38 -0
- flwr/client/nodestate/nodestate.py +30 -0
- flwr/client/nodestate/nodestate_factory.py +37 -0
- flwr/client/{node_state.py → run_info_store.py} +4 -3
- flwr/client/supernode/app.py +6 -8
- flwr/common/args.py +83 -0
- flwr/common/config.py +10 -0
- flwr/common/constant.py +39 -5
- flwr/common/context.py +9 -4
- flwr/common/date.py +3 -3
- flwr/common/logger.py +108 -1
- flwr/common/object_ref.py +47 -16
- flwr/common/serde.py +24 -0
- flwr/common/telemetry.py +0 -6
- flwr/common/typing.py +10 -1
- flwr/proto/exec_pb2.py +14 -17
- flwr/proto/exec_pb2.pyi +14 -22
- flwr/proto/log_pb2.py +29 -0
- flwr/proto/log_pb2.pyi +39 -0
- flwr/proto/log_pb2_grpc.py +4 -0
- flwr/proto/log_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +8 -8
- flwr/proto/message_pb2.pyi +4 -1
- flwr/proto/run_pb2.py +32 -27
- flwr/proto/run_pb2.pyi +26 -0
- flwr/proto/serverappio_pb2.py +52 -0
- flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +54 -0
- flwr/proto/serverappio_pb2_grpc.py +376 -0
- flwr/proto/serverappio_pb2_grpc.pyi +147 -0
- flwr/proto/simulationio_pb2.py +38 -0
- flwr/proto/simulationio_pb2.pyi +65 -0
- flwr/proto/simulationio_pb2_grpc.py +205 -0
- flwr/proto/simulationio_pb2_grpc.pyi +81 -0
- flwr/server/app.py +272 -105
- flwr/server/driver/driver.py +15 -1
- flwr/server/driver/grpc_driver.py +25 -36
- flwr/server/driver/inmemory_driver.py +6 -16
- flwr/server/run_serverapp.py +29 -23
- flwr/server/{superlink/state → serverapp}/__init__.py +3 -9
- flwr/server/serverapp/app.py +214 -0
- flwr/server/strategy/aggregate.py +4 -4
- flwr/server/strategy/fedadam.py +11 -1
- flwr/server/superlink/driver/__init__.py +1 -1
- flwr/server/superlink/driver/{driver_grpc.py → serverappio_grpc.py} +19 -16
- flwr/server/superlink/driver/{driver_servicer.py → serverappio_servicer.py} +125 -39
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +4 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -2
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -2
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -2
- flwr/server/superlink/fleet/message_handler/message_handler.py +7 -7
- flwr/server/superlink/fleet/rest_rere/rest_api.py +7 -7
- flwr/server/superlink/fleet/vce/vce_api.py +23 -23
- flwr/server/superlink/linkstate/__init__.py +28 -0
- flwr/server/superlink/{state/in_memory_state.py → linkstate/in_memory_linkstate.py} +184 -36
- flwr/server/superlink/{state/state.py → linkstate/linkstate.py} +149 -19
- flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +9 -9
- flwr/server/superlink/{state/sqlite_state.py → linkstate/sqlite_linkstate.py} +306 -65
- flwr/server/superlink/{state → linkstate}/utils.py +81 -30
- flwr/server/superlink/simulation/__init__.py +15 -0
- flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
- flwr/server/superlink/simulation/simulationio_servicer.py +153 -0
- flwr/simulation/__init__.py +5 -1
- flwr/simulation/app.py +273 -345
- flwr/simulation/legacy_app.py +382 -0
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -2
- flwr/simulation/run_simulation.py +57 -131
- flwr/simulation/simulationio_connection.py +86 -0
- flwr/superexec/app.py +6 -134
- flwr/superexec/deployment.py +61 -66
- flwr/superexec/exec_grpc.py +15 -8
- flwr/superexec/exec_servicer.py +36 -65
- flwr/superexec/executor.py +26 -7
- flwr/superexec/simulation.py +54 -107
- {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/METADATA +5 -4
- {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/RECORD +88 -69
- {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/entry_points.txt +2 -0
- flwr/client/node_state_tests.py +0 -66
- flwr/proto/driver_pb2.py +0 -42
- flwr/proto/driver_pb2_grpc.py +0 -239
- flwr/proto/driver_pb2_grpc.pyi +0 -94
- {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/WHEEL +0 -0
flwr/client/clientapp/app.py
CHANGED
|
@@ -24,6 +24,8 @@ import grpc
|
|
|
24
24
|
from flwr.cli.install import install_from_fab
|
|
25
25
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
|
26
26
|
from flwr.common import Context, Message
|
|
27
|
+
from flwr.common.args import add_args_flwr_app_common
|
|
28
|
+
from flwr.common.config import get_flwr_dir
|
|
27
29
|
from flwr.common.constant import ErrorCode
|
|
28
30
|
from flwr.common.grpc import create_channel
|
|
29
31
|
from flwr.common.logger import log
|
|
@@ -54,15 +56,13 @@ from .utils import get_load_client_app_fn
|
|
|
54
56
|
|
|
55
57
|
def flwr_clientapp() -> None:
|
|
56
58
|
"""Run process-isolated Flower ClientApp."""
|
|
57
|
-
log(INFO, "Starting Flower ClientApp")
|
|
58
|
-
|
|
59
59
|
parser = argparse.ArgumentParser(
|
|
60
60
|
description="Run a Flower ClientApp",
|
|
61
61
|
)
|
|
62
62
|
parser.add_argument(
|
|
63
63
|
"--supernode",
|
|
64
64
|
type=str,
|
|
65
|
-
help="Address of SuperNode ClientAppIo
|
|
65
|
+
help="Address of SuperNode's ClientAppIo API",
|
|
66
66
|
)
|
|
67
67
|
parser.add_argument(
|
|
68
68
|
"--token",
|
|
@@ -70,16 +70,24 @@ def flwr_clientapp() -> None:
|
|
|
70
70
|
required=False,
|
|
71
71
|
help="Unique token generated by SuperNode for each ClientApp execution",
|
|
72
72
|
)
|
|
73
|
+
add_args_flwr_app_common(parser=parser)
|
|
73
74
|
args = parser.parse_args()
|
|
74
75
|
|
|
76
|
+
log(INFO, "Starting Flower ClientApp")
|
|
77
|
+
|
|
75
78
|
log(
|
|
76
79
|
DEBUG,
|
|
77
|
-
"
|
|
80
|
+
"Starting isolated `ClientApp` connected to SuperNode's ClientAppIo API at %s "
|
|
78
81
|
"with token %s",
|
|
79
82
|
args.supernode,
|
|
80
83
|
args.token,
|
|
81
84
|
)
|
|
82
|
-
run_clientapp(
|
|
85
|
+
run_clientapp(
|
|
86
|
+
supernode=args.supernode,
|
|
87
|
+
run_once=(args.token is not None),
|
|
88
|
+
token=args.token,
|
|
89
|
+
flwr_dir=args.flwr_dir,
|
|
90
|
+
)
|
|
83
91
|
|
|
84
92
|
|
|
85
93
|
def on_channel_state_change(channel_connectivity: str) -> None:
|
|
@@ -89,27 +97,23 @@ def on_channel_state_change(channel_connectivity: str) -> None:
|
|
|
89
97
|
|
|
90
98
|
def run_clientapp( # pylint: disable=R0914
|
|
91
99
|
supernode: str,
|
|
100
|
+
run_once: bool,
|
|
92
101
|
token: Optional[int] = None,
|
|
102
|
+
flwr_dir: Optional[str] = None,
|
|
93
103
|
) -> None:
|
|
94
|
-
"""Run Flower ClientApp process.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
supernode : str
|
|
99
|
-
Address of SuperNode
|
|
100
|
-
token : Optional[int] (default: None)
|
|
101
|
-
Unique SuperNode token for ClientApp-SuperNode authentication
|
|
102
|
-
"""
|
|
104
|
+
"""Run Flower ClientApp process."""
|
|
103
105
|
channel = create_channel(
|
|
104
106
|
server_address=supernode,
|
|
105
107
|
insecure=True,
|
|
106
108
|
)
|
|
107
109
|
channel.subscribe(on_channel_state_change)
|
|
108
110
|
|
|
111
|
+
# Resolve directory where FABs are installed
|
|
112
|
+
flwr_dir_ = get_flwr_dir(flwr_dir)
|
|
113
|
+
|
|
109
114
|
try:
|
|
110
115
|
stub = ClientAppIoStub(channel)
|
|
111
116
|
|
|
112
|
-
only_once = token is not None
|
|
113
117
|
while True:
|
|
114
118
|
# If token is not set, loop until token is received from SuperNode
|
|
115
119
|
while token is None:
|
|
@@ -122,13 +126,13 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
122
126
|
# Install FAB, if provided
|
|
123
127
|
if fab:
|
|
124
128
|
log(DEBUG, "Flower ClientApp starts FAB installation.")
|
|
125
|
-
install_from_fab(fab.content, flwr_dir=
|
|
129
|
+
install_from_fab(fab.content, flwr_dir=flwr_dir_, skip_prompt=True)
|
|
126
130
|
|
|
127
131
|
load_client_app_fn = get_load_client_app_fn(
|
|
128
132
|
default_app_ref="",
|
|
129
133
|
app_path=None,
|
|
130
134
|
multi_app=True,
|
|
131
|
-
flwr_dir=
|
|
135
|
+
flwr_dir=str(flwr_dir_),
|
|
132
136
|
)
|
|
133
137
|
|
|
134
138
|
try:
|
|
@@ -170,7 +174,7 @@ def run_clientapp( # pylint: disable=R0914
|
|
|
170
174
|
|
|
171
175
|
# Stop the loop if `flwr-clientapp` is expected to process only a single
|
|
172
176
|
# message
|
|
173
|
-
if
|
|
177
|
+
if run_once:
|
|
174
178
|
break
|
|
175
179
|
|
|
176
180
|
except KeyboardInterrupt:
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
"""Flower NodeState."""
|
|
16
|
+
|
|
17
|
+
from .in_memory_nodestate import InMemoryNodeState as InMemoryNodeState
|
|
18
|
+
from .nodestate import NodeState as NodeState
|
|
19
|
+
from .nodestate_factory import NodeStateFactory as NodeStateFactory
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"InMemoryNodeState",
|
|
23
|
+
"NodeState",
|
|
24
|
+
"NodeStateFactory",
|
|
25
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
"""In-memory NodeState implementation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from flwr.client.nodestate.nodestate import NodeState
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InMemoryNodeState(NodeState):
|
|
24
|
+
"""In-memory NodeState implementation."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
# Store node_id
|
|
28
|
+
self.node_id: Optional[int] = None
|
|
29
|
+
|
|
30
|
+
def set_node_id(self, node_id: Optional[int]) -> None:
|
|
31
|
+
"""Set the node ID."""
|
|
32
|
+
self.node_id = node_id
|
|
33
|
+
|
|
34
|
+
def get_node_id(self) -> int:
|
|
35
|
+
"""Get the node ID."""
|
|
36
|
+
if self.node_id is None:
|
|
37
|
+
raise ValueError("Node ID not set")
|
|
38
|
+
return self.node_id
|
|
@@ -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
|
+
"""Abstract base class NodeState."""
|
|
16
|
+
|
|
17
|
+
import abc
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NodeState(abc.ABC):
|
|
22
|
+
"""Abstract NodeState."""
|
|
23
|
+
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def set_node_id(self, node_id: Optional[int]) -> None:
|
|
26
|
+
"""Set the node ID."""
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def get_node_id(self) -> int:
|
|
30
|
+
"""Get the node ID."""
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
"""Factory class that creates NodeState instances."""
|
|
16
|
+
|
|
17
|
+
import threading
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .in_memory_nodestate import InMemoryNodeState
|
|
21
|
+
from .nodestate import NodeState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NodeStateFactory:
|
|
25
|
+
"""Factory class that creates NodeState instances."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self.state_instance: Optional[NodeState] = None
|
|
29
|
+
self.lock = threading.RLock()
|
|
30
|
+
|
|
31
|
+
def state(self) -> NodeState:
|
|
32
|
+
"""Return a State instance and create it, if necessary."""
|
|
33
|
+
# Lock access to NodeStateFactory to prevent returning different instances
|
|
34
|
+
with self.lock:
|
|
35
|
+
if self.state_instance is None:
|
|
36
|
+
self.state_instance = InMemoryNodeState()
|
|
37
|
+
return self.state_instance
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
# ==============================================================================
|
|
15
|
-
"""
|
|
15
|
+
"""Deprecated Run Info Store."""
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from dataclasses import dataclass
|
|
@@ -36,7 +36,7 @@ class RunInfo:
|
|
|
36
36
|
initial_run_config: UserConfig
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
class
|
|
39
|
+
class DeprecatedRunInfoStore:
|
|
40
40
|
"""State of a node where client nodes execute runs."""
|
|
41
41
|
|
|
42
42
|
def __init__(
|
|
@@ -83,6 +83,7 @@ class NodeState:
|
|
|
83
83
|
self.run_infos[run_id] = RunInfo(
|
|
84
84
|
initial_run_config=initial_run_config,
|
|
85
85
|
context=Context(
|
|
86
|
+
run_id=run_id,
|
|
86
87
|
node_id=self.node_id,
|
|
87
88
|
node_config=self.node_config,
|
|
88
89
|
state=RecordSet(),
|
flwr/client/supernode/app.py
CHANGED
|
@@ -31,6 +31,8 @@ from flwr.common import EventType, event
|
|
|
31
31
|
from flwr.common.config import parse_config_args
|
|
32
32
|
from flwr.common.constant import (
|
|
33
33
|
FLEET_API_GRPC_RERE_DEFAULT_ADDRESS,
|
|
34
|
+
ISOLATION_MODE_PROCESS,
|
|
35
|
+
ISOLATION_MODE_SUBPROCESS,
|
|
34
36
|
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
35
37
|
TRANSPORT_TYPE_GRPC_RERE,
|
|
36
38
|
TRANSPORT_TYPE_REST,
|
|
@@ -38,11 +40,7 @@ from flwr.common.constant import (
|
|
|
38
40
|
from flwr.common.exit_handlers import register_exit_handlers
|
|
39
41
|
from flwr.common.logger import log, warn_deprecated_feature
|
|
40
42
|
|
|
41
|
-
from ..app import
|
|
42
|
-
ISOLATION_MODE_PROCESS,
|
|
43
|
-
ISOLATION_MODE_SUBPROCESS,
|
|
44
|
-
start_client_internal,
|
|
45
|
-
)
|
|
43
|
+
from ..app import start_client_internal
|
|
46
44
|
from ..clientapp.utils import get_load_client_app_fn
|
|
47
45
|
|
|
48
46
|
|
|
@@ -200,10 +198,10 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
|
|
|
200
198
|
ISOLATION_MODE_SUBPROCESS,
|
|
201
199
|
ISOLATION_MODE_PROCESS,
|
|
202
200
|
],
|
|
203
|
-
help="Isolation mode when running `ClientApp` (optional, possible values: "
|
|
204
|
-
"`subprocess`, `process`). By default, `ClientApp` runs in the same process "
|
|
201
|
+
help="Isolation mode when running a `ClientApp` (optional, possible values: "
|
|
202
|
+
"`subprocess`, `process`). By default, a `ClientApp` runs in the same process "
|
|
205
203
|
"that executes the SuperNode. Use `subprocess` to configure SuperNode to run "
|
|
206
|
-
"`ClientApp` in a subprocess. Use `process` to indicate that a separate "
|
|
204
|
+
"a `ClientApp` in a subprocess. Use `process` to indicate that a separate "
|
|
207
205
|
"independent process gets created outside of SuperNode.",
|
|
208
206
|
)
|
|
209
207
|
parser.add_argument(
|
flwr/common/args.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
"""Common Flower arguments."""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from logging import DEBUG, WARN
|
|
20
|
+
from os.path import isfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from flwr.common.logger import log
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
|
|
28
|
+
"""Add common Flower arguments for flwr-*app to the provided parser."""
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--flwr-dir",
|
|
31
|
+
default=None,
|
|
32
|
+
help="""The path containing installed Flower Apps.
|
|
33
|
+
By default, this value is equal to:
|
|
34
|
+
|
|
35
|
+
- `$FLWR_HOME/` if `$FLWR_HOME` is defined
|
|
36
|
+
- `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
|
|
37
|
+
- `$HOME/.flwr/` in all other cases
|
|
38
|
+
""",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--insecure",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Run the server without HTTPS, regardless of whether certificate "
|
|
44
|
+
"paths are provided. By default, the server runs with HTTPS enabled. "
|
|
45
|
+
"Use this flag only if you understand the risks.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--root-certificates",
|
|
49
|
+
metavar="ROOT_CERT",
|
|
50
|
+
type=str,
|
|
51
|
+
help="Specifies the path to the PEM-encoded root certificate file for "
|
|
52
|
+
"establishing secure HTTPS connections.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def try_obtain_certificates(
|
|
57
|
+
args: argparse.Namespace,
|
|
58
|
+
) -> Optional[bytes]:
|
|
59
|
+
"""Validate and return the root certificates."""
|
|
60
|
+
if args.insecure:
|
|
61
|
+
if args.root_certificates is not None:
|
|
62
|
+
sys.exit(
|
|
63
|
+
"Conflicting options: The '--insecure' flag disables HTTPS, "
|
|
64
|
+
"but '--root-certificates' was also specified. Please remove "
|
|
65
|
+
"the '--root-certificates' option when running in insecure mode, "
|
|
66
|
+
"or omit '--insecure' to use HTTPS."
|
|
67
|
+
)
|
|
68
|
+
log(
|
|
69
|
+
WARN,
|
|
70
|
+
"Option `--insecure` was set. Starting insecure HTTP channel.",
|
|
71
|
+
)
|
|
72
|
+
root_certificates = None
|
|
73
|
+
else:
|
|
74
|
+
# Load the certificates if provided, or load the system certificates
|
|
75
|
+
if not isfile(args.root_certificates):
|
|
76
|
+
sys.exit("Path argument `--root-certificates` does not point to a file.")
|
|
77
|
+
root_certificates = Path(args.root_certificates).read_bytes()
|
|
78
|
+
log(
|
|
79
|
+
DEBUG,
|
|
80
|
+
"Starting secure HTTPS channel with the following certificates: %s.",
|
|
81
|
+
args.root_certificates,
|
|
82
|
+
)
|
|
83
|
+
return root_certificates
|
flwr/common/config.py
CHANGED
|
@@ -22,6 +22,7 @@ from typing import Any, Optional, Union, cast, get_args
|
|
|
22
22
|
import tomli
|
|
23
23
|
|
|
24
24
|
from flwr.cli.config_utils import get_fab_config, validate_fields
|
|
25
|
+
from flwr.common import ConfigsRecord
|
|
25
26
|
from flwr.common.constant import (
|
|
26
27
|
APP_DIR,
|
|
27
28
|
FAB_CONFIG_FILE,
|
|
@@ -229,3 +230,12 @@ def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
|
|
|
229
230
|
config["project"]["version"],
|
|
230
231
|
f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
|
|
231
232
|
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
|
|
236
|
+
"""Construct a `ConfigsRecord` out of a `UserConfig`."""
|
|
237
|
+
c_record = ConfigsRecord()
|
|
238
|
+
for k, v in config.items():
|
|
239
|
+
c_record[k] = v
|
|
240
|
+
|
|
241
|
+
return c_record
|
flwr/common/constant.py
CHANGED
|
@@ -40,15 +40,15 @@ TRANSPORT_TYPES = [
|
|
|
40
40
|
# Addresses
|
|
41
41
|
# SuperNode
|
|
42
42
|
CLIENTAPPIO_API_DEFAULT_ADDRESS = "0.0.0.0:9094"
|
|
43
|
-
# SuperExec
|
|
44
|
-
EXEC_API_DEFAULT_ADDRESS = "0.0.0.0:9093"
|
|
45
43
|
# SuperLink
|
|
46
|
-
|
|
44
|
+
SERVERAPPIO_API_DEFAULT_ADDRESS = "0.0.0.0:9091"
|
|
47
45
|
FLEET_API_GRPC_RERE_DEFAULT_ADDRESS = "0.0.0.0:9092"
|
|
48
46
|
FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS = (
|
|
49
47
|
"[::]:8080" # IPv6 to keep start_server compatible
|
|
50
48
|
)
|
|
51
|
-
FLEET_API_REST_DEFAULT_ADDRESS = "0.0.0.0:
|
|
49
|
+
FLEET_API_REST_DEFAULT_ADDRESS = "0.0.0.0:9095"
|
|
50
|
+
EXEC_API_DEFAULT_ADDRESS = "0.0.0.0:9093"
|
|
51
|
+
SIMULATIONIO_API_DEFAULT_ADDRESS = "0.0.0.0:9096"
|
|
52
52
|
|
|
53
53
|
# Constants for ping
|
|
54
54
|
PING_DEFAULT_INTERVAL = 30
|
|
@@ -84,6 +84,16 @@ GRPC_ADAPTER_METADATA_MESSAGE_QUALNAME_KEY = "grpc-message-qualname"
|
|
|
84
84
|
# Message TTL
|
|
85
85
|
MESSAGE_TTL_TOLERANCE = 1e-1
|
|
86
86
|
|
|
87
|
+
# Isolation modes
|
|
88
|
+
ISOLATION_MODE_SUBPROCESS = "subprocess"
|
|
89
|
+
ISOLATION_MODE_PROCESS = "process"
|
|
90
|
+
|
|
91
|
+
# Log streaming configurations
|
|
92
|
+
CONN_REFRESH_PERIOD = 60 # Stream connection refresh period
|
|
93
|
+
CONN_RECONNECT_INTERVAL = 0.5 # Reconnect interval between two stream connections
|
|
94
|
+
LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `ExecServicer.StreamLogs`
|
|
95
|
+
LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
|
|
96
|
+
|
|
87
97
|
|
|
88
98
|
class MessageType:
|
|
89
99
|
"""Message type."""
|
|
@@ -124,8 +134,32 @@ class ErrorCode:
|
|
|
124
134
|
UNKNOWN = 0
|
|
125
135
|
LOAD_CLIENT_APP_EXCEPTION = 1
|
|
126
136
|
CLIENT_APP_RAISED_EXCEPTION = 2
|
|
127
|
-
NODE_UNAVAILABLE = 3
|
|
128
137
|
|
|
129
138
|
def __new__(cls) -> ErrorCode:
|
|
130
139
|
"""Prevent instantiation."""
|
|
131
140
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Status:
|
|
144
|
+
"""Run status."""
|
|
145
|
+
|
|
146
|
+
PENDING = "pending"
|
|
147
|
+
STARTING = "starting"
|
|
148
|
+
RUNNING = "running"
|
|
149
|
+
FINISHED = "finished"
|
|
150
|
+
|
|
151
|
+
def __new__(cls) -> Status:
|
|
152
|
+
"""Prevent instantiation."""
|
|
153
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class SubStatus:
|
|
157
|
+
"""Run sub-status."""
|
|
158
|
+
|
|
159
|
+
COMPLETED = "completed"
|
|
160
|
+
FAILED = "failed"
|
|
161
|
+
STOPPED = "stopped"
|
|
162
|
+
|
|
163
|
+
def __new__(cls) -> SubStatus:
|
|
164
|
+
"""Prevent instantiation."""
|
|
165
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
flwr/common/context.py
CHANGED
|
@@ -27,36 +27,41 @@ class Context:
|
|
|
27
27
|
|
|
28
28
|
Parameters
|
|
29
29
|
----------
|
|
30
|
+
run_id : int
|
|
31
|
+
The ID that identifies the run.
|
|
30
32
|
node_id : int
|
|
31
33
|
The ID that identifies the node.
|
|
32
34
|
node_config : UserConfig
|
|
33
35
|
A config (key/value mapping) unique to the node and independent of the
|
|
34
36
|
`run_config`. This config persists across all runs this node participates in.
|
|
35
37
|
state : RecordSet
|
|
36
|
-
Holds records added by the entity in a given
|
|
38
|
+
Holds records added by the entity in a given `run_id` and that will stay local.
|
|
37
39
|
This means that the data it holds will never leave the system it's running from.
|
|
38
40
|
This can be used as an intermediate storage or scratchpad when
|
|
39
41
|
executing mods. It can also be used as a memory to access
|
|
40
42
|
at different points during the lifecycle of this entity (e.g. across
|
|
41
43
|
multiple rounds)
|
|
42
44
|
run_config : UserConfig
|
|
43
|
-
A config (key/value mapping) held by the entity in a given
|
|
44
|
-
stay local. It can be used at any point during the lifecycle of this entity
|
|
45
|
+
A config (key/value mapping) held by the entity in a given `run_id` and that
|
|
46
|
+
will stay local. It can be used at any point during the lifecycle of this entity
|
|
45
47
|
(e.g. across multiple rounds)
|
|
46
48
|
"""
|
|
47
49
|
|
|
50
|
+
run_id: int
|
|
48
51
|
node_id: int
|
|
49
52
|
node_config: UserConfig
|
|
50
53
|
state: RecordSet
|
|
51
54
|
run_config: UserConfig
|
|
52
55
|
|
|
53
|
-
def __init__( # pylint: disable=too-many-arguments
|
|
56
|
+
def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
54
57
|
self,
|
|
58
|
+
run_id: int,
|
|
55
59
|
node_id: int,
|
|
56
60
|
node_config: UserConfig,
|
|
57
61
|
state: RecordSet,
|
|
58
62
|
run_config: UserConfig,
|
|
59
63
|
) -> None:
|
|
64
|
+
self.run_id = run_id
|
|
60
65
|
self.node_id = node_id
|
|
61
66
|
self.node_config = node_config
|
|
62
67
|
self.state = state
|
flwr/common/date.py
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"""Flower date utils."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import datetime
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def now() -> datetime:
|
|
21
|
+
def now() -> datetime.datetime:
|
|
22
22
|
"""Construct a datetime from time.time() with time zone set to UTC."""
|
|
23
|
-
return datetime.now(tz=timezone.utc)
|
|
23
|
+
return datetime.datetime.now(tz=datetime.timezone.utc)
|
flwr/common/logger.py
CHANGED
|
@@ -16,9 +16,22 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import logging
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
19
22
|
from logging import WARN, LogRecord
|
|
20
23
|
from logging.handlers import HTTPHandler
|
|
21
|
-
from
|
|
24
|
+
from queue import Empty, Queue
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Optional, TextIO, Union
|
|
26
|
+
|
|
27
|
+
import grpc
|
|
28
|
+
|
|
29
|
+
from flwr.proto.log_pb2 import PushLogsRequest # pylint: disable=E0611
|
|
30
|
+
from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
|
31
|
+
from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub # pylint: disable=E0611
|
|
32
|
+
from flwr.proto.simulationio_pb2_grpc import SimulationIoStub # pylint: disable=E0611
|
|
33
|
+
|
|
34
|
+
from .constant import LOG_UPLOAD_INTERVAL
|
|
22
35
|
|
|
23
36
|
# Create logger
|
|
24
37
|
LOGGER_NAME = "flwr"
|
|
@@ -259,3 +272,97 @@ def set_logger_propagation(
|
|
|
259
272
|
if not child_logger.propagate:
|
|
260
273
|
child_logger.log(logging.DEBUG, "Logger propagate set to False")
|
|
261
274
|
return child_logger
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def mirror_output_to_queue(log_queue: Queue[Optional[str]]) -> None:
|
|
278
|
+
"""Mirror stdout and stderr output to the provided queue."""
|
|
279
|
+
|
|
280
|
+
def get_write_fn(stream: TextIO) -> Any:
|
|
281
|
+
original_write = stream.write
|
|
282
|
+
|
|
283
|
+
def fn(s: str) -> int:
|
|
284
|
+
ret = original_write(s)
|
|
285
|
+
stream.flush()
|
|
286
|
+
log_queue.put(s)
|
|
287
|
+
return ret
|
|
288
|
+
|
|
289
|
+
return fn
|
|
290
|
+
|
|
291
|
+
sys.stdout.write = get_write_fn(sys.stdout) # type: ignore[method-assign]
|
|
292
|
+
sys.stderr.write = get_write_fn(sys.stderr) # type: ignore[method-assign]
|
|
293
|
+
console_handler.stream = sys.stdout
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def restore_output() -> None:
|
|
297
|
+
"""Restore stdout and stderr.
|
|
298
|
+
|
|
299
|
+
This will stop mirroring output to queues.
|
|
300
|
+
"""
|
|
301
|
+
sys.stdout = sys.__stdout__
|
|
302
|
+
sys.stderr = sys.__stderr__
|
|
303
|
+
console_handler.stream = sys.stdout
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _log_uploader(
|
|
307
|
+
log_queue: Queue[Optional[str]], node_id: int, run_id: int, stub: ServerAppIoStub
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Upload logs to the SuperLink."""
|
|
310
|
+
exit_flag = False
|
|
311
|
+
node = Node(node_id=node_id, anonymous=False)
|
|
312
|
+
msgs: list[str] = []
|
|
313
|
+
while True:
|
|
314
|
+
# Fetch all messages from the queue
|
|
315
|
+
try:
|
|
316
|
+
while True:
|
|
317
|
+
msg = log_queue.get_nowait()
|
|
318
|
+
# Quit the loops if the returned message is `None`
|
|
319
|
+
# This is a signal that the run has finished
|
|
320
|
+
if msg is None:
|
|
321
|
+
exit_flag = True
|
|
322
|
+
break
|
|
323
|
+
msgs.append(msg)
|
|
324
|
+
except Empty:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# Upload if any logs
|
|
328
|
+
if msgs:
|
|
329
|
+
req = PushLogsRequest(
|
|
330
|
+
node=node,
|
|
331
|
+
run_id=run_id,
|
|
332
|
+
logs=msgs,
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
stub.PushLogs(req)
|
|
336
|
+
msgs.clear()
|
|
337
|
+
except grpc.RpcError as e:
|
|
338
|
+
# Ignore minor network errors
|
|
339
|
+
# pylint: disable-next=no-member
|
|
340
|
+
if e.code() != grpc.StatusCode.UNAVAILABLE:
|
|
341
|
+
raise e
|
|
342
|
+
|
|
343
|
+
if exit_flag:
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
time.sleep(LOG_UPLOAD_INTERVAL)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def start_log_uploader(
|
|
350
|
+
log_queue: Queue[Optional[str]],
|
|
351
|
+
node_id: int,
|
|
352
|
+
run_id: int,
|
|
353
|
+
stub: Union[ServerAppIoStub, SimulationIoStub],
|
|
354
|
+
) -> threading.Thread:
|
|
355
|
+
"""Start the log uploader thread and return it."""
|
|
356
|
+
thread = threading.Thread(
|
|
357
|
+
target=_log_uploader, args=(log_queue, node_id, run_id, stub)
|
|
358
|
+
)
|
|
359
|
+
thread.start()
|
|
360
|
+
return thread
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def stop_log_uploader(
|
|
364
|
+
log_queue: Queue[Optional[str]], log_uploader: threading.Thread
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Stop the log uploader thread."""
|
|
367
|
+
log_queue.put(None)
|
|
368
|
+
log_uploader.join()
|