Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Middleware #2580

Merged
merged 124 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 107 commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
4886c77
Decouple client and app
danieljanes Sep 19, 2023
fb8ba3d
Merge branch 'main' into decouple-client
danieljanes Sep 28, 2023
fca2187
Merge branch 'main' into decouple-client
danieljanes Oct 13, 2023
5e5fec6
Merge branch 'main' into decouple-client
danieljanes Oct 14, 2023
c3e82b7
Merge branch 'main' into decouple-client
danieljanes Oct 16, 2023
1223f90
Shorten line
danieljanes Oct 16, 2023
555d89a
Merge branch 'main' into decouple-client
danieljanes Oct 25, 2023
a7c57d6
Add comments to main client loop
danieljanes Oct 25, 2023
9a20a0c
Refactor start_client
danieljanes Oct 30, 2023
d65601b
Merge branch 'main' into decouple-client
danieljanes Oct 30, 2023
82df0a3
Merge branch 'refactor-start-client' into decouple-client
danieljanes Oct 30, 2023
32e4973
Format code
danieljanes Oct 30, 2023
11bf509
Add type hint
danieljanes Oct 30, 2023
d097096
Merge branch 'refactor-start-client' into decouple-client
danieljanes Oct 30, 2023
a1a647a
Test with Iterator
charlesbvll Oct 30, 2023
ce2bbb8
Working solution (I think)
charlesbvll Oct 30, 2023
7617335
Fix imports
charlesbvll Oct 30, 2023
d9a7318
Merge branch 'refactor-start-client' into decouple-client
danieljanes Oct 30, 2023
46acb9d
Merge branch 'main' into decouple-client
danieljanes Oct 30, 2023
e61c91a
Merge branch 'main' into decouple-client
danieljanes Nov 1, 2023
156aad0
Move task execution into Flower app
danieljanes Nov 4, 2023
a47a760
Enable --app in flower-client
danieljanes Nov 4, 2023
77bfc04
Merge branch 'main' into decouple-client
danieljanes Nov 4, 2023
90058f9
Add comments to start_client
danieljanes Nov 4, 2023
86c1473
Handle control messages in a separate function
danieljanes Nov 4, 2023
9bc1349
Change comment
danieljanes Nov 4, 2023
ee87393
Merge branch 'refactor-start-client-comments' into refactor-start-cli…
danieljanes Nov 4, 2023
1691340
Update comments
danieljanes Nov 4, 2023
7d732e9
Update comments
danieljanes Nov 4, 2023
9f6472f
Format
danieljanes Nov 4, 2023
7e72fda
Merge branch 'refactor-start-client-comments' into refactor-start-cli…
danieljanes Nov 4, 2023
270b322
Merge branch 'refactor-start-client-control-message' into decouple-cl…
danieljanes Nov 5, 2023
c92ea66
Remove exception typing
danieljanes Nov 6, 2023
10ed750
Merge branch 'main' into refactor-start-client-control-message
danieljanes Nov 6, 2023
e37d2bf
Update message handler
danieljanes Nov 6, 2023
3d84159
Update tests
danieljanes Nov 6, 2023
58c2d96
Merge branch 'refactor-start-client-control-message' into decouple-cl…
danieljanes Nov 6, 2023
78356d0
Remove early exit
danieljanes Nov 6, 2023
7812c42
Return DisconnectRes to the server
danieljanes Nov 6, 2023
f30b014
Update tests
danieljanes Nov 6, 2023
5cf817b
Merge branch 'refactor-start-client-control-message' into decouple-cl…
danieljanes Nov 6, 2023
27f7099
Remove data field from Fwd/Bwd
danieljanes Nov 6, 2023
ebbb8d5
Remove Fwd/Bwd
danieljanes Nov 6, 2023
6bce092
Merge branch 'main' into decouple-client
danieljanes Nov 6, 2023
4a67ba0
Add type annotations
danieljanes Nov 6, 2023
61926b1
Merge branch 'main' into decouple-client
danieljanes Nov 6, 2023
0d3ad20
Merge branch 'main' into decouple-client
danieljanes Nov 6, 2023
daca6bc
Replace dict with WorkloadState
danieljanes Nov 6, 2023
37c74ac
Format
danieljanes Nov 6, 2023
faf5b47
Merge branch 'main' into decouple-client
danieljanes Nov 6, 2023
9b71c87
Merge branch 'main' into decouple-client
danieljanes Nov 6, 2023
dc5e7ce
Move Flower callable and run_client
danieljanes Nov 6, 2023
9cd55b7
Merge branch 'main' into decouple-client
danieljanes Nov 7, 2023
26a2245
Resolve merge conflict
danieljanes Nov 7, 2023
80b3cf2
Merge branch 'main' into decouple-client
danieljanes Nov 7, 2023
19e126b
Merge branch 'main' into decouple-client
danieljanes Nov 7, 2023
a7729ac
init
panh99 Nov 7, 2023
436ff40
add toy example
panh99 Nov 8, 2023
0d274da
Merge branch 'main' into decouple-client
danieljanes Nov 8, 2023
b6dfe7f
add doc
panh99 Nov 9, 2023
a21a0aa
add middleware.rst
panh99 Nov 9, 2023
d7a7752
Merge branch 'main' into decouple-client
danieljanes Nov 10, 2023
383f6ae
Add custom function to load Flower callable
danieljanes Nov 10, 2023
74f5959
Merge branch 'decouple-client' into middleware
panh99 Nov 11, 2023
aadfb19
update signature
panh99 Nov 11, 2023
c5f89c6
Merge branch 'main' into middleware
panh99 Nov 14, 2023
45a31df
update flower callable and fix cyclic imports
panh99 Nov 14, 2023
7976dbc
Add type annotation
danieljanes Nov 15, 2023
bad7f1f
Merge branch 'main' into decouple-client
danieljanes Nov 15, 2023
445b2f1
Merge branch 'decouple-client' into middleware
panh99 Nov 16, 2023
6753aab
amend doc
panh99 Nov 16, 2023
d748c18
Merge branch 'main' into middleware
panh99 Nov 16, 2023
90667e3
Merge branch 'main' into decouple-client
panh99 Nov 16, 2023
6613b87
Merge branch 'decouple-client' into middleware
panh99 Nov 16, 2023
c996328
Merge branch 'main' into decouple-client
danieljanes Nov 21, 2023
9a3cbfc
Merge branch 'decouple-client' of github.com:adap/flower into decoupl…
danieljanes Nov 21, 2023
65b4369
Remove --grpc-rere
danieljanes Nov 21, 2023
1107cad
Create mt-pytorch-app example
danieljanes Nov 21, 2023
22b5159
Reset quickstart-pytorch example
danieljanes Nov 21, 2023
ec6fcfc
Remove how-to-deploy doc
danieljanes Nov 21, 2023
c3dae4a
Rename --app to --callable
danieljanes Nov 21, 2023
77b2313
Rename mt-pytorch-app to mt-pytorch-callable
danieljanes Nov 21, 2023
edbe3cc
Finish renaming to callable
danieljanes Nov 21, 2023
81c13e3
Update docstring
danieljanes Nov 21, 2023
20f6f59
Rename callable to flower
danieljanes Nov 21, 2023
29e0c5a
Merge branch 'main' into decouple-client
danieljanes Nov 21, 2023
a1c6f64
Use experimental feature warning
danieljanes Nov 21, 2023
44c0129
Add README to mt-pytorch-callable
danieljanes Nov 21, 2023
b38226e
Merge branch 'main' into decouple-client
danieljanes Nov 21, 2023
578df87
Merge branch 'main' into decouple-client
danieljanes Nov 21, 2023
f05bd45
Merge branch 'decouple-client' into middleware
panh99 Nov 22, 2023
e3515a5
Merge branch 'main' into middleware
panh99 Nov 24, 2023
d0cc1f1
fix conflicts
panh99 Nov 24, 2023
45105f5
fix conflicts
panh99 Nov 24, 2023
fa3bf2c
update flower.py
panh99 Nov 24, 2023
6ffdb12
Merge branch 'main' into middleware
panh99 Nov 27, 2023
58aec35
fix cyclic import
panh99 Nov 27, 2023
f45cd97
use the same workload state in Fwd/Bwd
panh99 Nov 27, 2023
ae93cd9
Merge branch 'main' into middleware
panh99 Nov 27, 2023
0cbe49e
Merge branch 'main' into middleware
panh99 Nov 29, 2023
a3bb7ed
Merge branch 'main' into middleware
panh99 Nov 29, 2023
5d954af
update to main
panh99 Nov 29, 2023
3cc442a
update how-to tutorial
panh99 Nov 29, 2023
b214eb9
Merge branch 'main' into middleware
panh99 Nov 30, 2023
c4de6d6
Merge branch 'main' into middleware
panh99 Nov 30, 2023
8d97459
Merge branch 'middleware' of https://github.com/adap/flower into midd…
panh99 Nov 30, 2023
3861fb6
Update doc/source/how-to-use-middleware.rst
panh99 Nov 30, 2023
9a825ba
Update doc/source/how-to-use-middleware.rst
panh99 Nov 30, 2023
d4a9f7d
use FlowerCallable instead of App
panh99 Dec 1, 2023
2df4116
rename make_app to make_fc
panh99 Dec 1, 2023
2d1b983
fix typo
panh99 Dec 1, 2023
d897fe8
update naming
panh99 Dec 1, 2023
b8f25cd
Merge branch 'main' into middleware
panh99 Dec 1, 2023
c8fed7f
update tutorial
panh99 Dec 4, 2023
0f15a30
Merge branch 'middleware' of https://github.com/adap/flower into midd…
panh99 Dec 4, 2023
dba7c42
fix format
panh99 Dec 4, 2023
8daac9b
Merge branch 'main' into middleware
panh99 Dec 4, 2023
06d29c5
Merge branch 'main' into middleware
panh99 Dec 15, 2023
7088d8b
Merge branch 'main' into middleware
panh99 Dec 15, 2023
cb55d5d
Merge branch 'main' into middleware
panh99 Dec 18, 2023
d71f4e6
Update MW doc
panh99 Dec 19, 2023
8cd988b
update naming
panh99 Dec 19, 2023
69f40ab
Merge branch 'main' into middleware
panh99 Dec 19, 2023
1946484
Merge branch 'main' into middleware
danieljanes Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions doc/source/how-to-use-middleware.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Use Built-in Middleware Layers
panh99 marked this conversation as resolved.
Show resolved Hide resolved
==============================

panh99 marked this conversation as resolved.
Show resolved Hide resolved
In this tutorial, we will learn how to utilize built-in middleware layers to augment the behavior of an application. Middleware allows us to perform operations before and after a task is processed in the application.

What is Middleware?
-------------------

Middleware is a callable that wraps around an application. It can manipulate or inspect incoming tasks (``TaskIns``) in the ``Fwd`` and the resulting tasks (``TaskRes``) in the ``Bwd``. The signature for a middleware layer (``Layer``) is as follows:

.. code-block:: python
panh99 marked this conversation as resolved.
Show resolved Hide resolved

APP = Callable[[Fwd], Bwd]
panh99 marked this conversation as resolved.
Show resolved Hide resolved
Layer = Callable[[Fwd, App], Bwd]

A typical middleware function might look something like this:

.. code-block:: python

def example_middleware(fwd: Fwd, app: App) -> Bwd:
# Do something with Fwd before passing to app.
bwd = app(fwd)
# Do something with Bwd before returning.
return bwd

Using Middleware Layers
panh99 marked this conversation as resolved.
Show resolved Hide resolved
-----------------------

To use middleware layers in your application, you can follow these steps:

1. Import the Required Middleware
panh99 marked this conversation as resolved.
Show resolved Hide resolved
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

First, import the built-in middleware layers you intend to use:

.. code-block:: python

import flwr as fl
from flwr.client.middleware import example_middleware1, example_middleware2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do sth different here? This example won't work if anyone were to try it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have any built-in middleware for now. One alternative is to write simple example_middleware1 and example_middleware2 functions in the doc and use them. Wdyt?


2. Define Your Client Function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Define your client function (``client_fn``) that will be wrapped by the middleware:

.. code-block:: python

def client_fn():
# Your client code goes here.
pass

3. Create the Application with Middleware
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Create your application and pass the middleware layers as a list to the ``middleware`` argument. The order in which you provide the middleware layers matters:

.. code-block:: python

app = fl.app.Flower(
client_fn=client_fn,
middleware=[
example_middleware1, # Middleware layer 1
example_middleware2, # Middleware layer 2
]
)

Order of Execution
------------------

When the application runs, the middleware layers are executed in the order they are provided in the list:

1. ``example_middleware1`` (outermost layer)
2. ``example_middleware2`` (next layer)
3. Message handler (core app functionality)
4. ``example_middleware2`` (on the way back)
5. ``example_middleware1`` (outermost layer on the way back)

Each middleware has a chance to inspect and modify the ``TaskIns`` in the ``Fwd`` before passing it to the next layer, and likewise with the ``TaskRes`` in the ``Bwd`` before returning it up the stack.

Conclusion
----------

By following this guide, you have learned how to effectively use middleware layers to enhance your application's functionality. Remember that the order of middleware is crucial and affects how the input and output are processed.

Enjoy building more robust and flexible applications with middleware layers!
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Problem-oriented how-to guides show step-by-step how to achieve a specific goal.
how-to-configure-logging
how-to-enable-ssl-connections
how-to-upgrade-to-flower-1.0
how-to-use-middleware
panh99 marked this conversation as resolved.
Show resolved Hide resolved

.. toctree::
:maxdepth: 1
Expand Down
29 changes: 19 additions & 10 deletions src/py/flwr/client/flower.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@


import importlib
from typing import cast
from typing import List, Optional, cast

from flwr.client.message_handler.message_handler import handle
from flwr.client.middleware.typing import Layer
from flwr.client.middleware.utils import make_app
from flwr.client.typing import Bwd, ClientFn, Fwd


Expand Down Expand Up @@ -51,21 +53,28 @@ class Flower:
def __init__(
self,
client_fn: ClientFn, # Only for backward compatibility
middleware: Optional[List[Layer]] = None,
panh99 marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
self.client_fn = client_fn
self.mw_list = middleware if middleware is not None else []

def __call__(self, fwd: Fwd) -> Bwd:
"""."""

# Create wrapper function for `handle`
def handle_app(_fwd: Fwd) -> Bwd:
task_res, state_updated = handle(
client_fn=self.client_fn,
state=_fwd.state,
task_ins=_fwd.task_ins,
)
return Bwd(task_res=task_res, state=state_updated)

# Wrap middleware layers around handle_app
app = make_app(handle_app, self.mw_list)

# Execute the task
task_res, state_updated = handle(
client_fn=self.client_fn,
state=fwd.state,
task_ins=fwd.task_ins,
)
return Bwd(
task_res=task_res,
state=state_updated,
)
return app(fwd)


class LoadCallableError(Exception):
Expand Down
25 changes: 25 additions & 0 deletions src/py/flwr/client/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2023 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Middleware layers."""


from .typing import App, Layer
from .utils import make_app

__all__ = [
"App",
"Layer",
"make_app",
]
22 changes: 22 additions & 0 deletions src/py/flwr/client/middleware/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2023 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Types for middleware layers."""

from typing import Callable

from flwr.client.typing import Bwd, Fwd

App = Callable[[Fwd], Bwd]
Layer = Callable[[Fwd, App], Bwd]
panh99 marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 36 additions & 0 deletions src/py/flwr/client/middleware/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2023 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Utility functions for middleware layers."""
danieljanes marked this conversation as resolved.
Show resolved Hide resolved

from typing import List

from flwr.client.typing import Bwd, Fwd

from .typing import App, Layer


def make_app(app: App, middleware_layers: List[Layer]) -> App:
"""."""

def wrap_app(_app: App, _layer: Layer) -> App:
def new_app(fwd: Fwd) -> Bwd:
return _layer(fwd, _app)

return new_app

for layer in reversed(middleware_layers):
app = wrap_app(app, layer)

return app
100 changes: 100 additions & 0 deletions src/py/flwr/client/middleware/utils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright 2023 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for the utility functions."""


import unittest
from typing import List

from flwr.client.typing import Bwd, Fwd
from flwr.client.workload_state import WorkloadState
from flwr.proto.task_pb2 import TaskIns, TaskRes

from .typing import App, Layer
from .utils import make_app


def make_mock_middleware(name: str, footprint: List[str]) -> Layer:
"""Make a mock middleware layer."""

def middleware(fwd: Fwd, app: App) -> Bwd:
footprint.append(name)
fwd.task_ins.task_id += f"{name}"
bwd = app(fwd)
footprint.append(name)
bwd.task_res.task_id += f"{name}"
return bwd

return middleware


def make_mock_app(name: str, footprint: List[str]) -> App:
"""Make a mock app."""

def app(fwd: Fwd) -> Bwd:
footprint.append(name)
fwd.task_ins.task_id += f"{name}"
return Bwd(task_res=TaskRes(task_id=name), state=WorkloadState({}))

return app


class TestMakeApp(unittest.TestCase):
"""Tests for the `make_app` function."""

def test_multiple_middlewares(self) -> None:
"""Test if multiple middlewares are called in the correct order."""
# Prepare
footprint: List[str] = []
mock_app = make_mock_app("app", footprint)
mock_middleware_names = [f"middleware{i}" for i in range(1, 15)]
mock_middleware_layers = [
make_mock_middleware(name, footprint) for name in mock_middleware_names
]
task_ins = TaskIns()

# Execute
wrapped_app = make_app(mock_app, mock_middleware_layers)
task_res = wrapped_app(Fwd(task_ins=task_ins, state=WorkloadState({}))).task_res

# Assert
trace = mock_middleware_names + ["app"]
self.assertEqual(footprint, trace + list(reversed(mock_middleware_names)))
# pylint: disable-next=no-member
self.assertEqual(task_ins.task_id, "".join(trace))
self.assertEqual(task_res.task_id, "".join(reversed(trace)))

def test_filter(self) -> None:
"""Test if a middleware can filter incoming TaskIns."""
# Prepare
footprint: List[str] = []
mock_app = make_mock_app("app", footprint)
task_ins = TaskIns()

def filter_layer(fwd: Fwd, _: App) -> Bwd:
footprint.append("filter")
fwd.task_ins.task_id += "filter"
# Skip calling app
return Bwd(task_res=TaskRes(task_id="filter"), state=WorkloadState({}))

# Execute
wrapped_app = make_app(mock_app, [filter_layer])
task_res = wrapped_app(Fwd(task_ins=task_ins, state=WorkloadState({}))).task_res

# Assert
self.assertEqual(footprint, ["filter"])
# pylint: disable-next=no-member
self.assertEqual(task_ins.task_id, "filter")
self.assertEqual(task_res.task_id, "filter")