flwr-nightly 1.20.0.dev20250722__py3-none-any.whl → 1.20.0.dev20250724__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/common/exit/exit_code.py +5 -0
- flwr/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +35 -0
- flwr/proto/clientappio_pb2_grpc.pyi +14 -0
- flwr/server/grid/grpc_grid.py +51 -21
- flwr/server/superlink/utils.py +0 -27
- flwr/supercore/scheduler/__init__.py +22 -0
- flwr/supercore/scheduler/plugin.py +71 -0
- flwr/supernode/scheduler/__init__.py +22 -0
- flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +18 -0
- flwr/supernode/start_client_internal.py +10 -5
- {flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/METADATA +1 -1
- {flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/RECORD +16 -12
- {flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/entry_points.txt +0 -0
flwr/common/exit/exit_code.py
CHANGED
@@ -31,6 +31,7 @@ class ExitCode:
|
|
31
31
|
SUPERLINK_THREAD_CRASH = 100
|
32
32
|
SUPERLINK_LICENSE_INVALID = 101
|
33
33
|
SUPERLINK_LICENSE_MISSING = 102
|
34
|
+
SUPERLINK_LICENSE_URL_INVALID = 103
|
34
35
|
|
35
36
|
# ServerApp-specific exit codes (200-299)
|
36
37
|
|
@@ -70,6 +71,10 @@ EXIT_CODE_HELP = {
|
|
70
71
|
"The license is missing. Please specify the license key by setting the "
|
71
72
|
"environment variable `FLWR_LICENSE_KEY`."
|
72
73
|
),
|
74
|
+
ExitCode.SUPERLINK_LICENSE_URL_INVALID: (
|
75
|
+
"The license URL is invalid. Please ensure that the `FLWR_LICENSE_URL` "
|
76
|
+
"environment variable is set to a valid URL."
|
77
|
+
),
|
73
78
|
# ServerApp-specific exit codes (200-299)
|
74
79
|
# SuperNode-specific exit codes (300-399)
|
75
80
|
ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
|
flwr/proto/clientappio_pb2.py
CHANGED
@@ -18,7 +18,7 @@ from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2
|
|
18
18
|
from flwr.proto import appio_pb2 as flwr_dot_proto_dot_appio__pb2
|
19
19
|
|
20
20
|
|
21
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\x1a\x16\x66lwr/proto/appio.proto\"%\n#GetRunIdsWithPendingMessagesRequest\"7\n$GetRunIdsWithPendingMessagesResponse\x12\x0f\n\x07run_ids\x18\x01 \x03(\x04\"%\n\x13RequestTokenRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"%\n\x14RequestTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t2\
|
21
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66lwr/proto/clientappio.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x18\x66lwr/proto/message.proto\x1a\x16\x66lwr/proto/appio.proto\"%\n#GetRunIdsWithPendingMessagesRequest\"7\n$GetRunIdsWithPendingMessagesResponse\x12\x0f\n\x07run_ids\x18\x01 \x03(\x04\"%\n\x13RequestTokenRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"%\n\x14RequestTokenResponse\x12\r\n\x05token\x18\x01 \x01(\t2\xaf\x07\n\x0b\x43lientAppIo\x12\x83\x01\n\x1cGetRunIdsWithPendingMessages\x12/.flwr.proto.GetRunIdsWithPendingMessagesRequest\x1a\x30.flwr.proto.GetRunIdsWithPendingMessagesResponse\"\x00\x12S\n\x0cRequestToken\x12\x1f.flwr.proto.RequestTokenRequest\x1a .flwr.proto.RequestTokenResponse\"\x00\x12\x41\n\x06GetRun\x12\x19.flwr.proto.GetRunRequest\x1a\x1a.flwr.proto.GetRunResponse\"\x00\x12\\\n\x13PullClientAppInputs\x12 .flwr.proto.PullAppInputsRequest\x1a!.flwr.proto.PullAppInputsResponse\"\x00\x12_\n\x14PushClientAppOutputs\x12!.flwr.proto.PushAppOutputsRequest\x1a\".flwr.proto.PushAppOutputsResponse\"\x00\x12X\n\x0bPushMessage\x12\".flwr.proto.PushAppMessagesRequest\x1a#.flwr.proto.PushAppMessagesResponse\"\x00\x12X\n\x0bPullMessage\x12\".flwr.proto.PullAppMessagesRequest\x1a#.flwr.proto.PullAppMessagesResponse\"\x00\x12M\n\nPushObject\x12\x1d.flwr.proto.PushObjectRequest\x1a\x1e.flwr.proto.PushObjectResponse\"\x00\x12M\n\nPullObject\x12\x1d.flwr.proto.PullObjectRequest\x1a\x1e.flwr.proto.PullObjectResponse\"\x00\x12q\n\x16\x43onfirmMessageReceived\x12).flwr.proto.ConfirmMessageReceivedRequest\x1a*.flwr.proto.ConfirmMessageReceivedResponse\"\x00\x62\x06proto3')
|
22
22
|
|
23
23
|
_globals = globals()
|
24
24
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
@@ -34,5 +34,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
34
34
|
_globals['_REQUESTTOKENRESPONSE']._serialized_start=273
|
35
35
|
_globals['_REQUESTTOKENRESPONSE']._serialized_end=310
|
36
36
|
_globals['_CLIENTAPPIO']._serialized_start=313
|
37
|
-
_globals['_CLIENTAPPIO']._serialized_end=
|
37
|
+
_globals['_CLIENTAPPIO']._serialized_end=1256
|
38
38
|
# @@protoc_insertion_point(module_scope)
|
@@ -5,6 +5,7 @@ import grpc
|
|
5
5
|
from flwr.proto import appio_pb2 as flwr_dot_proto_dot_appio__pb2
|
6
6
|
from flwr.proto import clientappio_pb2 as flwr_dot_proto_dot_clientappio__pb2
|
7
7
|
from flwr.proto import message_pb2 as flwr_dot_proto_dot_message__pb2
|
8
|
+
from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
|
8
9
|
|
9
10
|
|
10
11
|
class ClientAppIoStub(object):
|
@@ -26,6 +27,11 @@ class ClientAppIoStub(object):
|
|
26
27
|
request_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.SerializeToString,
|
27
28
|
response_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.FromString,
|
28
29
|
)
|
30
|
+
self.GetRun = channel.unary_unary(
|
31
|
+
'/flwr.proto.ClientAppIo/GetRun',
|
32
|
+
request_serializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString,
|
33
|
+
response_deserializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString,
|
34
|
+
)
|
29
35
|
self.PullClientAppInputs = channel.unary_unary(
|
30
36
|
'/flwr.proto.ClientAppIo/PullClientAppInputs',
|
31
37
|
request_serializer=flwr_dot_proto_dot_appio__pb2.PullAppInputsRequest.SerializeToString,
|
@@ -80,6 +86,13 @@ class ClientAppIoServicer(object):
|
|
80
86
|
context.set_details('Method not implemented!')
|
81
87
|
raise NotImplementedError('Method not implemented!')
|
82
88
|
|
89
|
+
def GetRun(self, request, context):
|
90
|
+
"""Get run details
|
91
|
+
"""
|
92
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
93
|
+
context.set_details('Method not implemented!')
|
94
|
+
raise NotImplementedError('Method not implemented!')
|
95
|
+
|
83
96
|
def PullClientAppInputs(self, request, context):
|
84
97
|
"""Pull client app inputs
|
85
98
|
"""
|
@@ -142,6 +155,11 @@ def add_ClientAppIoServicer_to_server(servicer, server):
|
|
142
155
|
request_deserializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenRequest.FromString,
|
143
156
|
response_serializer=flwr_dot_proto_dot_clientappio__pb2.RequestTokenResponse.SerializeToString,
|
144
157
|
),
|
158
|
+
'GetRun': grpc.unary_unary_rpc_method_handler(
|
159
|
+
servicer.GetRun,
|
160
|
+
request_deserializer=flwr_dot_proto_dot_run__pb2.GetRunRequest.FromString,
|
161
|
+
response_serializer=flwr_dot_proto_dot_run__pb2.GetRunResponse.SerializeToString,
|
162
|
+
),
|
145
163
|
'PullClientAppInputs': grpc.unary_unary_rpc_method_handler(
|
146
164
|
servicer.PullClientAppInputs,
|
147
165
|
request_deserializer=flwr_dot_proto_dot_appio__pb2.PullAppInputsRequest.FromString,
|
@@ -221,6 +239,23 @@ class ClientAppIo(object):
|
|
221
239
|
options, channel_credentials,
|
222
240
|
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
223
241
|
|
242
|
+
@staticmethod
|
243
|
+
def GetRun(request,
|
244
|
+
target,
|
245
|
+
options=(),
|
246
|
+
channel_credentials=None,
|
247
|
+
call_credentials=None,
|
248
|
+
insecure=False,
|
249
|
+
compression=None,
|
250
|
+
wait_for_ready=None,
|
251
|
+
timeout=None,
|
252
|
+
metadata=None):
|
253
|
+
return grpc.experimental.unary_unary(request, target, '/flwr.proto.ClientAppIo/GetRun',
|
254
|
+
flwr_dot_proto_dot_run__pb2.GetRunRequest.SerializeToString,
|
255
|
+
flwr_dot_proto_dot_run__pb2.GetRunResponse.FromString,
|
256
|
+
options, channel_credentials,
|
257
|
+
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
258
|
+
|
224
259
|
@staticmethod
|
225
260
|
def PullClientAppInputs(request,
|
226
261
|
target,
|
@@ -6,6 +6,7 @@ import abc
|
|
6
6
|
import flwr.proto.appio_pb2
|
7
7
|
import flwr.proto.clientappio_pb2
|
8
8
|
import flwr.proto.message_pb2
|
9
|
+
import flwr.proto.run_pb2
|
9
10
|
import grpc
|
10
11
|
|
11
12
|
class ClientAppIoStub:
|
@@ -20,6 +21,11 @@ class ClientAppIoStub:
|
|
20
21
|
flwr.proto.clientappio_pb2.RequestTokenResponse]
|
21
22
|
"""Request token"""
|
22
23
|
|
24
|
+
GetRun: grpc.UnaryUnaryMultiCallable[
|
25
|
+
flwr.proto.run_pb2.GetRunRequest,
|
26
|
+
flwr.proto.run_pb2.GetRunResponse]
|
27
|
+
"""Get run details"""
|
28
|
+
|
23
29
|
PullClientAppInputs: grpc.UnaryUnaryMultiCallable[
|
24
30
|
flwr.proto.appio_pb2.PullAppInputsRequest,
|
25
31
|
flwr.proto.appio_pb2.PullAppInputsResponse]
|
@@ -73,6 +79,14 @@ class ClientAppIoServicer(metaclass=abc.ABCMeta):
|
|
73
79
|
"""Request token"""
|
74
80
|
pass
|
75
81
|
|
82
|
+
@abc.abstractmethod
|
83
|
+
def GetRun(self,
|
84
|
+
request: flwr.proto.run_pb2.GetRunRequest,
|
85
|
+
context: grpc.ServicerContext,
|
86
|
+
) -> flwr.proto.run_pb2.GetRunResponse:
|
87
|
+
"""Get run details"""
|
88
|
+
pass
|
89
|
+
|
76
90
|
@abc.abstractmethod
|
77
91
|
def PullClientAppInputs(self,
|
78
92
|
request: flwr.proto.appio_pb2.PullAppInputsRequest,
|
flwr/server/grid/grpc_grid.py
CHANGED
@@ -22,10 +22,13 @@ from typing import Optional, cast
|
|
22
22
|
|
23
23
|
import grpc
|
24
24
|
|
25
|
-
from flwr.
|
25
|
+
from flwr.app.error import Error
|
26
|
+
from flwr.common import Message, Metadata, RecordDict, now
|
26
27
|
from flwr.common.constant import (
|
27
28
|
SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
|
28
29
|
SUPERLINK_NODE_ID,
|
30
|
+
ErrorCode,
|
31
|
+
MessageType,
|
29
32
|
)
|
30
33
|
from flwr.common.grpc import create_channel, on_channel_state_change
|
31
34
|
from flwr.common.inflatable import (
|
@@ -39,12 +42,13 @@ from flwr.common.inflatable_protobuf_utils import (
|
|
39
42
|
make_push_object_fn_protobuf,
|
40
43
|
)
|
41
44
|
from flwr.common.inflatable_utils import (
|
45
|
+
ObjectUnavailableError,
|
42
46
|
inflate_object_from_contents,
|
43
47
|
pull_objects,
|
44
48
|
push_objects,
|
45
49
|
)
|
46
50
|
from flwr.common.logger import log, warn_deprecated_feature
|
47
|
-
from flwr.common.message import remove_content_from_message
|
51
|
+
from flwr.common.message import make_message, remove_content_from_message
|
48
52
|
from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
|
49
53
|
from flwr.common.serde import message_to_proto, run_from_proto
|
50
54
|
from flwr.common.typing import Run
|
@@ -260,15 +264,15 @@ class GrpcGrid(Grid):
|
|
260
264
|
run_id = cast(Run, self._run).run_id
|
261
265
|
message_ids: list[str] = []
|
262
266
|
try:
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
267
|
+
with no_object_id_recompute():
|
268
|
+
for msg in messages:
|
269
|
+
# Populate metadata
|
270
|
+
msg.metadata.__dict__["_run_id"] = run_id
|
271
|
+
msg.metadata.__dict__["_src_node_id"] = self.node.node_id
|
272
|
+
msg.metadata.__dict__["_message_id"] = msg.object_id
|
273
|
+
# Check message
|
274
|
+
self._check_message(msg)
|
275
|
+
# Try pushing message and its objects
|
272
276
|
message_ids.append(self._try_push_message(run_id, msg))
|
273
277
|
|
274
278
|
except grpc.RpcError as e:
|
@@ -307,16 +311,42 @@ class GrpcGrid(Grid):
|
|
307
311
|
inflated_msgs: list[Message] = []
|
308
312
|
for msg_proto, msg_tree in zip(res.messages_list, res.message_object_trees):
|
309
313
|
msg_id = msg_proto.metadata.message_id
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
314
|
+
try:
|
315
|
+
all_object_contents = pull_objects(
|
316
|
+
object_ids=[
|
317
|
+
tree.object_id for tree in iterate_object_tree(msg_tree)
|
318
|
+
],
|
319
|
+
pull_object_fn=make_pull_object_fn_protobuf(
|
320
|
+
pull_object_protobuf=self._stub.PullObject,
|
321
|
+
node=self.node,
|
322
|
+
run_id=run_id,
|
323
|
+
),
|
324
|
+
)
|
325
|
+
except ObjectUnavailableError as e:
|
326
|
+
# An ObjectUnavailableError indicates that the object is not yet
|
327
|
+
# available. If this point has been reached, it means that the
|
328
|
+
# Grid has tried to pull the object for the maximum number of times
|
329
|
+
# or for the maximum time allowed, so we return an inflated message
|
330
|
+
# with an error
|
331
|
+
inflated_msgs.append(
|
332
|
+
make_message(
|
333
|
+
metadata=Metadata(
|
334
|
+
run_id=run_id,
|
335
|
+
message_id="",
|
336
|
+
src_node_id=self.node.node_id,
|
337
|
+
dst_node_id=self.node.node_id,
|
338
|
+
message_type=MessageType.SYSTEM,
|
339
|
+
group_id="",
|
340
|
+
ttl=0,
|
341
|
+
reply_to_message_id=msg_proto.metadata.reply_to_message_id,
|
342
|
+
created_at=now().timestamp(),
|
343
|
+
),
|
344
|
+
error=Error(
|
345
|
+
code=ErrorCode.MESSAGE_UNAVAILABLE, reason=(str(e))
|
346
|
+
),
|
347
|
+
)
|
348
|
+
)
|
349
|
+
continue
|
320
350
|
|
321
351
|
# Confirm that the message has been received
|
322
352
|
self._stub.ConfirmMessageReceived(
|
flwr/server/superlink/utils.py
CHANGED
@@ -21,9 +21,6 @@ import grpc
|
|
21
21
|
|
22
22
|
from flwr.common.constant import Status, SubStatus
|
23
23
|
from flwr.common.typing import RunStatus
|
24
|
-
from flwr.proto.appio_pb2 import PushAppMessagesRequest # pylint: disable=E0611
|
25
|
-
from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
|
26
|
-
from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
|
27
24
|
from flwr.server.superlink.linkstate import LinkState
|
28
25
|
from flwr.supercore.object_store import ObjectStore
|
29
26
|
|
@@ -73,27 +70,3 @@ def abort_if(
|
|
73
70
|
"""Abort context if status of the provided `run_id` is in `abort_status_list`."""
|
74
71
|
msg = check_abort(run_id, abort_status_list, state, store)
|
75
72
|
abort_grpc_context(msg, context)
|
76
|
-
|
77
|
-
|
78
|
-
def store_mapping_and_register_objects(
|
79
|
-
store: ObjectStore, request: Union[PushAppMessagesRequest, PushMessagesRequest]
|
80
|
-
) -> dict[str, ObjectIDs]:
|
81
|
-
"""Store Message object to descendants mapping and preregister objects."""
|
82
|
-
if not request.messages_list:
|
83
|
-
return {}
|
84
|
-
|
85
|
-
objects_to_push: dict[str, ObjectIDs] = {}
|
86
|
-
|
87
|
-
# Get run_id from the first message in the list
|
88
|
-
# All messages of a request should in the same run
|
89
|
-
run_id = request.messages_list[0].metadata.run_id
|
90
|
-
|
91
|
-
for object_tree in request.message_object_trees:
|
92
|
-
# Preregister
|
93
|
-
object_ids_just_registered = store.preregister(run_id, object_tree)
|
94
|
-
# Keep track of objects that need to be pushed
|
95
|
-
objects_to_push[object_tree.object_id] = ObjectIDs(
|
96
|
-
object_ids=object_ids_just_registered
|
97
|
-
)
|
98
|
-
|
99
|
-
return objects_to_push
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright 2025 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 App Scheduler."""
|
16
|
+
|
17
|
+
|
18
|
+
from .plugin import SchedulerPlugin
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"SchedulerPlugin",
|
22
|
+
]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright 2025 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 SchedulerPlugin."""
|
16
|
+
|
17
|
+
|
18
|
+
from abc import ABC, abstractmethod
|
19
|
+
from collections.abc import Sequence
|
20
|
+
from typing import Callable, Optional
|
21
|
+
|
22
|
+
from flwr.common.typing import Run
|
23
|
+
|
24
|
+
|
25
|
+
class SchedulerPlugin(ABC):
|
26
|
+
"""Abstract base class for Scheduler plugins."""
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
appio_api_address: str,
|
31
|
+
flwr_dir: str,
|
32
|
+
get_run: Callable[[int], Run],
|
33
|
+
) -> None:
|
34
|
+
self.appio_api_address = appio_api_address
|
35
|
+
self.flwr_dir = flwr_dir
|
36
|
+
self.get_run = get_run
|
37
|
+
|
38
|
+
@abstractmethod
|
39
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
|
40
|
+
"""Select a run ID to execute from a sequence of candidates.
|
41
|
+
|
42
|
+
A candidate run ID is one that has at least one pending message and is
|
43
|
+
not currently in progress (i.e., not associated with a token).
|
44
|
+
|
45
|
+
Parameters
|
46
|
+
----------
|
47
|
+
candidate_run_ids : Sequence[int]
|
48
|
+
A sequence of candidate run IDs to choose from.
|
49
|
+
|
50
|
+
Returns
|
51
|
+
-------
|
52
|
+
Optional[int]
|
53
|
+
The selected run ID, or None if no suitable candidate is found.
|
54
|
+
"""
|
55
|
+
|
56
|
+
@abstractmethod
|
57
|
+
def launch_app(self, token: str, run_id: int) -> None:
|
58
|
+
"""Launch the application associated with a given run ID and token.
|
59
|
+
|
60
|
+
This method starts the application process using the given `token`.
|
61
|
+
The `run_id` is used solely for bookkeeping purposes, allowing any
|
62
|
+
scheduler implementation to associate this launch with a specific run.
|
63
|
+
|
64
|
+
Parameters
|
65
|
+
----------
|
66
|
+
token : str
|
67
|
+
The token required to run the application.
|
68
|
+
run_id : int
|
69
|
+
The ID of the run associated with the token, used for tracking or
|
70
|
+
logging purposes.
|
71
|
+
"""
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright 2025 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 ClientApp Scheduler."""
|
16
|
+
|
17
|
+
|
18
|
+
from .simple_clientapp_scheduler_plugin import SimpleClientAppSchedulerPlugin
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"SimpleClientAppSchedulerPlugin",
|
22
|
+
]
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright 2025 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
|
+
"""Simple Flower ClientApp Scheduler plugin."""
|
16
|
+
|
17
|
+
|
18
|
+
import os
|
19
|
+
import subprocess
|
20
|
+
from collections.abc import Sequence
|
21
|
+
from typing import Optional
|
22
|
+
|
23
|
+
from flwr.supercore.scheduler import SchedulerPlugin
|
24
|
+
|
25
|
+
|
26
|
+
class SimpleClientAppSchedulerPlugin(SchedulerPlugin):
|
27
|
+
"""Simple Flower ClientApp Scheduler plugin.
|
28
|
+
|
29
|
+
The plugin always selects the first candidate run ID.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
|
33
|
+
"""Select a run ID to execute from a sequence of candidates."""
|
34
|
+
if not candidate_run_ids:
|
35
|
+
return None
|
36
|
+
return candidate_run_ids[0]
|
37
|
+
|
38
|
+
def launch_app(self, token: str, run_id: int) -> None:
|
39
|
+
"""Launch the application associated with a given run ID and token."""
|
40
|
+
cmds = ["flwr-clientapp", "--insecure"]
|
41
|
+
cmds += ["--clientappio-api-address", self.appio_api_address]
|
42
|
+
cmds += ["--token", token]
|
43
|
+
cmds += ["--parent-pid", str(os.getpid())]
|
44
|
+
cmds += ["--flwr-dir", self.flwr_dir]
|
45
|
+
# Launch the client app without waiting for it to complete.
|
46
|
+
# Since we don't need to manage the process, we intentionally avoid using
|
47
|
+
# a `with` statement. Suppress the pylint warning for it in this case.
|
48
|
+
# pylint: disable-next=consider-using-with
|
49
|
+
subprocess.Popen(cmds)
|
@@ -59,6 +59,7 @@ from flwr.proto.message_pb2 import (
|
|
59
59
|
PushObjectRequest,
|
60
60
|
PushObjectResponse,
|
61
61
|
)
|
62
|
+
from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=E0611
|
62
63
|
|
63
64
|
# pylint: disable=E0601
|
64
65
|
from flwr.supercore.ffs import FfsFactory
|
@@ -118,6 +119,23 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
118
119
|
# Return the token
|
119
120
|
return RequestTokenResponse(token=token)
|
120
121
|
|
122
|
+
def GetRun(
|
123
|
+
self, request: GetRunRequest, context: grpc.ServicerContext
|
124
|
+
) -> GetRunResponse:
|
125
|
+
"""Get run information."""
|
126
|
+
log(DEBUG, "ClientAppIo.GetRun")
|
127
|
+
|
128
|
+
# Initialize state connection
|
129
|
+
state = self.state_factory.state()
|
130
|
+
|
131
|
+
# Retrieve run information
|
132
|
+
run = state.get_run(request.run_id)
|
133
|
+
|
134
|
+
if run is None:
|
135
|
+
return GetRunResponse()
|
136
|
+
|
137
|
+
return GetRunResponse(run=run_to_proto(run))
|
138
|
+
|
121
139
|
def PullClientAppInputs(
|
122
140
|
self, request: PullAppInputsRequest, context: grpc.ServicerContext
|
123
141
|
) -> PullAppInputsResponse:
|
@@ -237,6 +237,12 @@ def start_client_internal(
|
|
237
237
|
]
|
238
238
|
subprocess.run(command, check=False)
|
239
239
|
|
240
|
+
# No message has been pulled therefore we can skip the push stage.
|
241
|
+
if run_id is None:
|
242
|
+
# If no message was received, wait for a while
|
243
|
+
time.sleep(3)
|
244
|
+
continue
|
245
|
+
|
240
246
|
_push_messages(
|
241
247
|
state=state,
|
242
248
|
object_store=store,
|
@@ -244,9 +250,6 @@ def start_client_internal(
|
|
244
250
|
push_object=push_object,
|
245
251
|
)
|
246
252
|
|
247
|
-
# Sleep for 3 seconds before the next iteration
|
248
|
-
time.sleep(3)
|
249
|
-
|
250
253
|
|
251
254
|
def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
|
252
255
|
state: NodeState,
|
@@ -358,8 +361,10 @@ def _push_messages(
|
|
358
361
|
push_object: Callable[[int, str, bytes], None],
|
359
362
|
) -> None:
|
360
363
|
"""Push reply messages to the SuperLink."""
|
361
|
-
#
|
362
|
-
|
364
|
+
# This is to ensure that only one message is processed at a time
|
365
|
+
# Wait until a reply message is available
|
366
|
+
while not (reply_messages := state.get_messages(is_reply=True)):
|
367
|
+
time.sleep(0.5)
|
363
368
|
|
364
369
|
for message in reply_messages:
|
365
370
|
# Log message sending
|
{flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: flwr-nightly
|
3
|
-
Version: 1.20.0.
|
3
|
+
Version: 1.20.0.dev20250724
|
4
4
|
Summary: Flower: A Friendly Federated AI Framework
|
5
5
|
License: Apache-2.0
|
6
6
|
Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
|
{flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/RECORD
RENAMED
@@ -118,7 +118,7 @@ flwr/common/event_log_plugin/__init__.py,sha256=ts3VAL3Fk6Grp1EK_1Qg_V-BfOof9F86
|
|
118
118
|
flwr/common/event_log_plugin/event_log_plugin.py,sha256=eK8OaDFagQRwqpb9eV0cJcm2ErtEBpMxFbhxJNx6n5w,2061
|
119
119
|
flwr/common/exit/__init__.py,sha256=-ZOJYLaNnR729a7VzZiFsLiqngzKQh3xc27svYStZ_Q,826
|
120
120
|
flwr/common/exit/exit.py,sha256=mJgbqMlVlwAgYtq-Vedj53wO4VxcDcy_P-GzqGK-1GQ,3452
|
121
|
-
flwr/common/exit/exit_code.py,sha256=
|
121
|
+
flwr/common/exit/exit_code.py,sha256=NbjJEywRb5YzDE7Xmo3Kd29inDEQbIUpskY9IEIGomE,4110
|
122
122
|
flwr/common/exit_handlers.py,sha256=IaqJ60fXZuu7McaRYnoYKtlbH9t4Yl9goNExKqtmQbs,4304
|
123
123
|
flwr/common/grpc.py,sha256=y70hUFvXkIf3l03xOhlb7qhS6W1UJZRSZqCdB0ir0v8,10381
|
124
124
|
flwr/common/heartbeat.py,sha256=SyEpNDnmJ0lni0cWO67rcoJVKasCLmkNHm3dKLeNrLU,5749
|
@@ -168,10 +168,10 @@ flwr/proto/appio_pb2.py,sha256=wCmsFJphNn5VHV2ew6-Wz58OclbsNfiwkwbssX8Ojf0,3853
|
|
168
168
|
flwr/proto/appio_pb2.pyi,sha256=_75EI4-BtZFW_tQcjLYhULmZPlFh1WJGVpL4vOROljU,7736
|
169
169
|
flwr/proto/appio_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
170
170
|
flwr/proto/appio_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
|
171
|
-
flwr/proto/clientappio_pb2.py,sha256=
|
171
|
+
flwr/proto/clientappio_pb2.py,sha256=3b87t4_AFgeDopgpYhhOVBrdh1HG74W2HC-y0C0q_eY,3349
|
172
172
|
flwr/proto/clientappio_pb2.pyi,sha256=gA-WU5lCYxhMciAUiCg__DrAzHPtytqRLyflpToWHqc,2033
|
173
|
-
flwr/proto/clientappio_pb2_grpc.py,sha256=
|
174
|
-
flwr/proto/clientappio_pb2_grpc.pyi,sha256=
|
173
|
+
flwr/proto/clientappio_pb2_grpc.py,sha256=CrB1caDEuPxgjQxDEozLyApzNMqzD8z1pWg310xCTGU,17856
|
174
|
+
flwr/proto/clientappio_pb2_grpc.pyi,sha256=FkS3ZcsU1iUuoS7ji84o8L5D9u-e5vsVR7O-ESqD5LI,4939
|
175
175
|
flwr/proto/error_pb2.py,sha256=PQVWrfjVPo88ql_KgV9nCxyQNCcV9PVfmcw7sOzTMro,1084
|
176
176
|
flwr/proto/error_pb2.pyi,sha256=ZNH4HhJTU_KfMXlyCeg8FwU-fcUYxTqEmoJPtWtHikc,734
|
177
177
|
flwr/proto/error_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
@@ -242,7 +242,7 @@ flwr/server/criterion.py,sha256=G4e-6B48Pc7d5rmGVUpIzNKb6UF88O3VmTRuUltgjzM,1061
|
|
242
242
|
flwr/server/fleet_event_log_interceptor.py,sha256=gtVPr2yQp3U2GnabIdFnuok18VerEcidKj1lKmDYGoY,3950
|
243
243
|
flwr/server/grid/__init__.py,sha256=aWZHezoR2UGMJISB_gPMCm2N_2GSbm97A3lAp7ruhRQ,888
|
244
244
|
flwr/server/grid/grid.py,sha256=naGCYt5J6dnmUvrcGkdNyKPe3MBd-0awGm1ALmgahqY,6625
|
245
|
-
flwr/server/grid/grpc_grid.py,sha256=
|
245
|
+
flwr/server/grid/grpc_grid.py,sha256=anUMektTtlAWcvu6wf7dsoiuBF5OiXfKuginHx0RWT0,15292
|
246
246
|
flwr/server/grid/inmemory_grid.py,sha256=RjejYT-d-hHuTs1KSs_5wvOdAWKLus8w5_UAcnGt4iw,6168
|
247
247
|
flwr/server/history.py,sha256=cCkFhBN4GoHsYYNk5GG1Y089eKJh2DH_ZJbYPwLaGyk,5026
|
248
248
|
flwr/server/run_serverapp.py,sha256=v0p6jXj2dFxlRUdoEeF1mnaFd9XRQi6dZCflPY6d3qI,2063
|
@@ -309,7 +309,7 @@ flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=m0c05XQwRWwymOq
|
|
309
309
|
flwr/server/superlink/simulation/__init__.py,sha256=Ry8DrNaZCMcQXvUc4FoCN2m3dvUQgWjasfp015o3Ec4,718
|
310
310
|
flwr/server/superlink/simulation/simulationio_grpc.py,sha256=VqWKxjpd4bCgPFKsgtIZPk9YcG0kc1EEmr5k20EKty4,2205
|
311
311
|
flwr/server/superlink/simulation/simulationio_servicer.py,sha256=m1T1zvEn81jlfx9hVTqmeWxAu6APCS2YW8l5O0OQvhU,7724
|
312
|
-
flwr/server/superlink/utils.py,sha256=
|
312
|
+
flwr/server/superlink/utils.py,sha256=zXmyU2o535b9dgz-TvFklzfuQk4irNnMtiK8vT4Dm1c,2454
|
313
313
|
flwr/server/typing.py,sha256=LvO6gq7H6TAWhA9JFx0WyqHxU7FycyvhSsLjBLPgpts,1011
|
314
314
|
flwr/server/utils/__init__.py,sha256=U4gM84-uUFddarODDQkO6SjNUuGhFcsHJZMjSEbezkU,884
|
315
315
|
flwr/server/utils/tensorboard.py,sha256=3z3MeF0cu_U6ghNgRd0UQ5bunyDQKxCLpIpEdGMoCJ0,5466
|
@@ -344,6 +344,8 @@ flwr/supercore/object_store/in_memory_object_store.py,sha256=CGY43syxDGrUPcdOzRH
|
|
344
344
|
flwr/supercore/object_store/object_store.py,sha256=J-rI3X7ET-F6dqOyM-UfHKCCQtPJ_EnYW62H_1txts0,5252
|
345
345
|
flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
|
346
346
|
flwr/supercore/object_store/utils.py,sha256=-WwBa6ejMNm9ahmNZP39IHutS0cwingmeqCoxTmATQM,1845
|
347
|
+
flwr/supercore/scheduler/__init__.py,sha256=E4GviiNJoZKz1dOao8ZGRHExsiM23GtOrkpMrTHy3n8,787
|
348
|
+
flwr/supercore/scheduler/plugin.py,sha256=kIv0JUrHP-ghrcGT-pbporL9A2mUz8PxASw6KgxCRv8,2460
|
347
349
|
flwr/supercore/utils.py,sha256=ebuHMbeA8eXisX0oMPqBK3hk7uVnIE_yiqWVz8YbkpQ,1324
|
348
350
|
flwr/superexec/__init__.py,sha256=YFqER0IJc1XEWfsX6AxZ9LSRq0sawPYrNYki-brvTIc,715
|
349
351
|
flwr/superexec/app.py,sha256=U2jjOHb2LGWoU7vrl9_czTzre9O2mPxu3CPGUZ86sK4,1465
|
@@ -366,11 +368,13 @@ flwr/supernode/nodestate/nodestate.py,sha256=kkGFxYnLIwT4-UmlPnf6HvAUpPey2urUNrw
|
|
366
368
|
flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
|
367
369
|
flwr/supernode/runtime/__init__.py,sha256=JQdqd2EMTn-ORMeTvewYYh52ls0YKP68jrps1qioxu4,718
|
368
370
|
flwr/supernode/runtime/run_clientapp.py,sha256=woAO8rXclt5eZeNHokhBChgxMf-TAzqWnHCkoiSsLVs,10765
|
371
|
+
flwr/supernode/scheduler/__init__.py,sha256=nQLi5ROVCMz8ii_WsZn4MAqKHXI40Eb3tq5-9zZbmpg,850
|
372
|
+
flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py,sha256=nnNuKhelrAEGCnWZ3aAkqJrhPu7xlVQ-oO1Eih9shTU,2000
|
369
373
|
flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca8gxdEo,717
|
370
374
|
flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
|
371
|
-
flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=
|
372
|
-
flwr/supernode/start_client_internal.py,sha256=
|
373
|
-
flwr_nightly-1.20.0.
|
374
|
-
flwr_nightly-1.20.0.
|
375
|
-
flwr_nightly-1.20.0.
|
376
|
-
flwr_nightly-1.20.0.
|
375
|
+
flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=ClPoKco7Tjj_cxRPhZlQSrOvcGa8sJwGs26LUNZnI3Y,10608
|
376
|
+
flwr/supernode/start_client_internal.py,sha256=VC7rV4QaX5Vk4Lw0DDodhwsKvGiHbr0mqrV0H77h1UI,21999
|
377
|
+
flwr_nightly-1.20.0.dev20250724.dist-info/METADATA,sha256=q7m01BPLI60ilXLKUeH5jYl-Eet8A98qD9F-sAK5mdg,15966
|
378
|
+
flwr_nightly-1.20.0.dev20250724.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
379
|
+
flwr_nightly-1.20.0.dev20250724.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
|
380
|
+
flwr_nightly-1.20.0.dev20250724.dist-info/RECORD,,
|
{flwr_nightly-1.20.0.dev20250722.dist-info → flwr_nightly-1.20.0.dev20250724.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|