Skip to content

Commit

Permalink
Merge branch 'main' into add-userinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
chongshenng committed Feb 26, 2025
2 parents 8d49f83 + bfe5e76 commit 24355c7
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 74 deletions.
145 changes: 110 additions & 35 deletions framework/docs/source/how-to-use-built-in-mods.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
Use Built-in Mods
=================

**Note: This tutorial covers experimental features. The functionality and interfaces may
change in future versions.**
.. note::

This tutorial covers preview features. The functionality and interfaces may change
in future versions.

In this tutorial, we will learn how to utilize built-in mods to augment the behavior of
a ``ClientApp``. Mods (sometimes also called Modifiers) allow us to perform operations
Expand All @@ -21,80 +23,153 @@ is as follows:

.. code-block:: python
ClientApp = Callable[[Message, Context], Message]
Mod = Callable[[Message, Context, ClientApp], Message]
ClientAppCallable = Callable[[Message, Context], Message]
Mod = Callable[[Message, Context, ClientAppCallable], Message]
A typical mod function might look something like this:

.. code-block:: python
def example_mod(msg: Message, ctx: Context, nxt: ClientApp) -> Message:
from flwr.client.typing import ClientAppCallable
from flwr.common import Context, Message
def example_mod(msg: Message, ctx: Context, call_next: ClientAppCallable) -> Message:
# Do something with incoming Message (or Context)
# before passing to the inner ``ClientApp``
msg = nxt(msg, ctx)
# before passing it to the next layer in the chain.
# This could be another Mod or, if this is the last Mod, the ClientApp itself.
msg = call_next(msg, ctx)
# Do something with outgoing Message (or Context)
# before returning
return msg
Using Mods
----------

To use mods in your ``ClientApp``, you can follow these steps:
Mods can be registered in two ways: **Application-wide mods** and **Function-specific
mods**.

1. **Application-wide mods**: These mods apply to all functions within the
``ClientApp``.
2. **Function-specific mods**: These mods apply only to a specific function (e.g, the
function decorated by ``@app.train()``)

1. Registering Application-wide Mods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. Import the required mods
~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use application-wide mods in your ``ClientApp``, follow these steps:

First, import the built-in mod you intend to use:
Import the required mods
++++++++++++++++++++++++

.. code-block:: python
import flwr as fl
from flwr.client.mod import example_mod_1, example_mod_2
2. Define your client function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

.. code-block:: python
def client_fn(cid):
# Your client code goes here.
return # your client
3. Create the ``ClientApp`` with mods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create the ``ClientApp`` with application-wide mods
+++++++++++++++++++++++++++++++++++++++++++++++++++

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

.. code-block:: python
app = fl.client.ClientApp(
client_fn=client_fn,
client_fn=client_fn, # Not needed if using decorators
mods=[
example_mod_1, # Mod 1
example_mod_2, # Mod 2
example_mod_1, # Application-wide Mod 1
example_mod_2, # Application-wide Mod 2
],
)
Order of execution
If you define functions to handle messages using decorators instead of ``client_fn``,
e.g., ``@app.train()``, you do not need to pass the ``client_fn`` argument.

2. Registering Function-specific Mods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Instead of applying mods to the entire ``ClientApp``, you can specify them for a
particular function:

.. code-block:: python
import flwr as fl
from flwr.client.mod import example_mod_3, example_mod_4
app = fl.client.ClientApp()
@app.train(mods=[example_mod_3, example_mod_4])
def train(msg, ctx):
# Training logic here
return reply_msg
@app.evaluate()
def evaluate(msg, ctx):
# Evaluation logic here
return reply_msg
In this case, ``example_mod_3`` and ``example_mod_4`` are only applied to the ``train``
function.

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

When the ``ClientApp`` runs, the mods are executed in the order they are provided in the
list:
When the ``ClientApp`` runs, the mods execute in the following order:

1. ``example_mod_1`` (outermost mod)
2. ``example_mod_2`` (next mod)
3. Message handler (core function that handles the incoming ``Message`` and returns the
1. **Application-wide mods** (executed first, in the order they are provided)
2. **Function-specific mods** (executed after application-wide mods, in the order they
are provided)
3. **ClientApp** (core function that handles the incoming ``Message`` and returns the
outgoing ``Message``)
4. ``example_mod_2`` (on the way back)
5. ``example_mod_1`` (outermost mod on the way back)
4. **Function-specific mods** (on the way back, in reverse order)
5. **Application-wide mods** (on the way back, in reverse order)

Each mod has a chance to inspect and modify the incoming ``Message`` before passing it
to the next mod, and likewise with the outgoing ``Message`` before returning it up the
stack.

Example Execution Flow
~~~~~~~~~~~~~~~~~~~~~~

Assuming the following registration:

.. code-block:: python
app = fl.client.ClientApp(mods=[example_mod_1, example_mod_2])
@app.train(mods=[example_mod_3, example_mod_4])
def train(msg, ctx):
return msg.create_reply(fl.common.RecordSet())
@app.evaluate()
def evaluate(msg, ctx):
return msg.create_reply(fl.common.RecordSet())
The execution order for an incoming **train** message is as follows:

1. ``example_mod_1`` (before handling)
2. ``example_mod_2`` (before handling)
3. ``example_mod_3`` (before handling)
4. ``example_mod_4`` (before handling)
5. ``train`` (handling message)
6. ``example_mod_4`` (after handling)
7. ``example_mod_3`` (after handling)
8. ``example_mod_2`` (after handling)
9. ``example_mod_1`` (after handling)

The execution order for an incoming **evaluate** message is as follows:

1. ``example_mod_1`` (before handling)
2. ``example_mod_2`` (before handling)
3. ``evaluate`` (handling message)
4. ``example_mod_2`` (after handling)
5. ``example_mod_1`` (after handling)

Conclusion
----------

Expand Down
59 changes: 53 additions & 6 deletions src/py/flwr/client/client_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,33 @@ def __call__(self, message: Message, context: Context) -> Message:
# Message type did not match one of the known message types abvoe
raise ValueError(f"Unknown message_type: {message.metadata.message_type}")

def train(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
def train(
self, mods: Optional[list[Mod]] = None
) -> Callable[[ClientAppCallable], ClientAppCallable]:
"""Return a decorator that registers the train fn with the client app.
Examples
--------
Registering a train function:
>>> app = ClientApp()
>>>
>>> @app.train()
>>> def train(message: Message, context: Context) -> Message:
>>> print("ClientApp training running")
>>> # Create and return an echo reply message
>>> return message.create_reply(content=message.content())
Registering a train function with a function-specific modifier:
>>> from flwr.client.mod import message_size_mod
>>>
>>> app = ClientApp()
>>>
>>> @app.train(mods=[message_size_mod])
>>> def train(message: Message, context: Context) -> Message:
>>> print("ClientApp training running with message size mod")
>>> return message.create_reply(content=message.content())
"""

def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:
Expand All @@ -182,25 +197,41 @@ def train_decorator(train_fn: ClientAppCallable) -> ClientAppCallable:

# Register provided function with the ClientApp object
# Wrap mods around the wrapped step function
self._train = make_ffn(train_fn, self._mods)
self._train = make_ffn(train_fn, self._mods + (mods or []))

# Return provided function unmodified
return train_fn

return train_decorator

def evaluate(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
def evaluate(
self, mods: Optional[list[Mod]] = None
) -> Callable[[ClientAppCallable], ClientAppCallable]:
"""Return a decorator that registers the evaluate fn with the client app.
Examples
--------
Registering an evaluate function:
>>> app = ClientApp()
>>>
>>> @app.evaluate()
>>> def evaluate(message: Message, context: Context) -> Message:
>>> print("ClientApp evaluation running")
>>> # Create and return an echo reply message
>>> return message.create_reply(content=message.content())
Registering an evaluate function with a function-specific modifier:
>>> from flwr.client.mod import message_size_mod
>>>
>>> app = ClientApp()
>>>
>>> @app.evaluate(mods=[message_size_mod])
>>> def evaluate(message: Message, context: Context) -> Message:
>>> print("ClientApp evaluation running with message size mod")
>>> # Create and return an echo reply message
>>> return message.create_reply(content=message.content())
"""

def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:
Expand All @@ -212,25 +243,41 @@ def evaluate_decorator(evaluate_fn: ClientAppCallable) -> ClientAppCallable:

# Register provided function with the ClientApp object
# Wrap mods around the wrapped step function
self._evaluate = make_ffn(evaluate_fn, self._mods)
self._evaluate = make_ffn(evaluate_fn, self._mods + (mods or []))

# Return provided function unmodified
return evaluate_fn

return evaluate_decorator

def query(self) -> Callable[[ClientAppCallable], ClientAppCallable]:
def query(
self, mods: Optional[list[Mod]] = None
) -> Callable[[ClientAppCallable], ClientAppCallable]:
"""Return a decorator that registers the query fn with the client app.
Examples
--------
Registering a query function:
>>> app = ClientApp()
>>>
>>> @app.query()
>>> def query(message: Message, context: Context) -> Message:
>>> print("ClientApp query running")
>>> # Create and return an echo reply message
>>> return message.create_reply(content=message.content())
Registering a query function with a function-specific modifier:
>>> from flwr.client.mod import message_size_mod
>>>
>>> app = ClientApp()
>>>
>>> @app.query(mods=[message_size_mod])
>>> def query(message: Message, context: Context) -> Message:
>>> print("ClientApp query running with message size mod")
>>> # Create and return an echo reply message
>>> return message.create_reply(content=message.content())
"""

def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:
Expand All @@ -242,7 +289,7 @@ def query_decorator(query_fn: ClientAppCallable) -> ClientAppCallable:

# Register provided function with the ClientApp object
# Wrap mods around the wrapped step function
self._query = make_ffn(query_fn, self._mods)
self._query = make_ffn(query_fn, self._mods + (mods or []))

# Return provided function unmodified
return query_fn
Expand Down
25 changes: 8 additions & 17 deletions src/py/flwr/common/record/conversion_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,17 @@
"""Conversion utility functions for Records."""


from io import BytesIO

import numpy as np

from ..constant import SType
from ..logger import warn_deprecated_feature
from ..typing import NDArray
from .parametersrecord import Array

WARN_DEPRECATED_MESSAGE = (
"`array_from_numpy` is deprecated. Instead, use the `Array(ndarray)` class "
"directly or `Array.from_numpy_ndarray(ndarray)`."
)


def array_from_numpy(ndarray: NDArray) -> Array:
"""Create Array from NumPy ndarray."""
buffer = BytesIO()
# WARNING: NEVER set allow_pickle to true.
# Reason: loading pickled data can execute arbitrary code
# Source: https://numpy.org/doc/stable/reference/generated/numpy.save.html
np.save(buffer, ndarray, allow_pickle=False)
data = buffer.getvalue()
return Array(
dtype=str(ndarray.dtype),
shape=list(ndarray.shape),
stype=SType.NUMPY,
data=data,
)
warn_deprecated_feature(WARN_DEPRECATED_MESSAGE)
return Array.from_numpy_ndarray(ndarray)
Loading

0 comments on commit 24355c7

Please sign in to comment.