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

Virtual Client Simulation #781

Merged
merged 33 commits into from Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7547798
Create RayClientProxy
danieljanes May 19, 2021
a986136
Merge branch 'main' into virtualclient
tanertopal Jun 4, 2021
eb15031
first working (still wip) implementation
jafermarq Jun 6, 2021
4fa63d8
Merge branch 'virtualclient' of https://github.com/adap/flower into v…
jafermarq Jun 6, 2021
ccc884e
ray.init() moved to start_ray_simulation()
jafermarq Jun 15, 2021
93634ff
Merge branch 'main' into virtualclient
danieljanes Jun 25, 2021
56fee06
Add optional ray dependency
danieljanes Jun 25, 2021
542162f
added simulation_ray example
jafermarq Jul 1, 2021
04d89ea
Create simulation quickstart example
danieljanes Jul 1, 2021
159cbde
Merge branch 'main' into virtualclient
danieljanes Jul 1, 2021
2c2bdc4
Merge branch 'main' into virtualclient
jafermarq Jul 15, 2021
0c131a1
new directory for simulation code; minor updates to pytorch/tf examples
jafermarq Jul 15, 2021
13302de
fix for compatibility with latest Ray1.4.1
jafermarq Jul 15, 2021
99703e8
Minor updates to pytorch example; added comments
jafermarq Jul 21, 2021
1d4e1d9
Remove Ray examples
danieljanes Jul 22, 2021
0ce4303
Update module docstrings
danieljanes Jul 22, 2021
2e8a57e
Auto-format code
danieljanes Jul 22, 2021
ff847b3
Test if ray is importable
danieljanes Jul 22, 2021
ccf9208
Remove duplicate code
danieljanes Jul 22, 2021
020ec8c
Refactor RayClientProxy to be compatible with both Client and NumPyCl…
danieljanes Jul 23, 2021
f46c5e1
Merge branch 'main' into vcm0
danieljanes Jul 23, 2021
a3a4bbc
Resolve lint issues
danieljanes Jul 23, 2021
acb6560
Improve optional Ray import
danieljanes Jul 24, 2021
ba1b19f
Add proper docstrings
danieljanes Jul 24, 2021
f678953
Refactor start_simulation arguments
danieljanes Jul 24, 2021
11d7957
Fix CI
tanertopal Jul 25, 2021
65dea96
Merge branch 'vcm0' of github.com:adap/flower into vcm0
tanertopal Jul 25, 2021
acadae9
Merge branch 'vcm0' of github.com:adap/flower into vcm0
danieljanes Jul 25, 2021
a4af129
Add simulation extra to scripts
danieljanes Jul 25, 2021
6eba278
Merge branch 'main' into vcm0
tanertopal Jul 25, 2021
6b35f70
Ignore type of optional ray import
danieljanes Jul 25, 2021
3417d1d
Merge branch 'main' into vcm0
tanertopal Jul 26, 2021
aa78da5
Update docstrings, rename ray package
danieljanes Jul 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Expand Up @@ -21,7 +21,7 @@ jobs:
python -m pip install -U poetry==1.1.6
poetry config virtualenvs.create false
- name: Install dependencies (mandatory only)
run: python -m poetry install --extras "baseline examples-tensorflow examples-pytorch http-logger ops"
run: python -m poetry install --extras "baseline examples-tensorflow examples-pytorch http-logger ops simulation"
- name: Build and deploy docs
env:
AWS_DEFAULT_REGION: ${{ secrets. AWS_DEFAULT_REGION }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/flower.yml
Expand Up @@ -28,7 +28,7 @@ jobs:
python -m pip install -U poetry==1.1.6
poetry config virtualenvs.create false
- name: Install dependencies (mandatory only)
run: python -m poetry install
run: python -m poetry install --extras "simulation"
- name: Check if protos need recompilation
run: ./dev/check-protos.sh
- name: Lint + Test (isort/black/docformatter/mypy/pylint/flake8/pytest)
Expand Down
3 changes: 2 additions & 1 deletion dev/bootstrap.sh
Expand Up @@ -16,4 +16,5 @@ python -m poetry install \
--extras "examples-pytorch" \
--extras "examples-tensorflow" \
--extras "http-logger" \
--extras "ops"
--extras "ops" \
--extras "simulation"
2 changes: 2 additions & 0 deletions pyproject.toml
Expand Up @@ -64,8 +64,10 @@ paramiko = { version = "^2.7.1", optional = true }
docker = { version = "^4.2.0", optional = true }
matplotlib = { version = "^3.2.1", optional = true }
tqdm = { version = "^4.48.2", optional = true }
ray = { version = "^1.4.0", optional = true}

[tool.poetry.extras]
simulation = ["ray"]
baseline = ["tensorflow-cpu", "boto3", "boto3_type_annotations", "paramiko", "docker", "matplotlib"]
examples-pytorch = ["torch", "torchvision", "tqdm"]
examples-tensorflow = ["tensorflow-cpu"]
Expand Down
3 changes: 2 additions & 1 deletion src/py/flwr/__init__.py
Expand Up @@ -15,9 +15,10 @@
"""Flower main package."""


from . import client, server
from . import client, server, simulation

__all__ = [
"client",
"server",
"simulation",
]
2 changes: 1 addition & 1 deletion src/py/flwr/client/__init__.py
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower Client."""
"""Flower client."""


from .app import start_client as start_client
Expand Down
2 changes: 1 addition & 1 deletion src/py/flwr/dataset/__init__.py
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower Datasets."""
"""Flower dataset."""
2 changes: 1 addition & 1 deletion src/py/flwr/server/__init__.py
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower Server."""
"""Flower server."""


from .app import start_server as start_server
Expand Down
2 changes: 1 addition & 1 deletion src/py/flwr/server/grpc_server/grpc_client_proxy.py
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Networked Flower client implementation."""
"""gRPC-based Flower ClientProxy implementation."""


from flwr import common
Expand Down
15 changes: 15 additions & 0 deletions src/py/flwr/server/ray_server/__init__.py
@@ -0,0 +1,15 @@
# Copyright 2020 Adap 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.
# ==============================================================================
"""Server for Ray-based ClientProxy objects."""
22 changes: 22 additions & 0 deletions src/py/flwr/simulation/__init__.py
@@ -0,0 +1,22 @@
# Copyright 2020 Adap 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.
# ==============================================================================
"""Flower simulation."""


from .app import start_simulation as start_simulation

__all__ = [
"start_simulation",
]
106 changes: 106 additions & 0 deletions src/py/flwr/simulation/app.py
@@ -0,0 +1,106 @@
# Copyright 2020 Adap 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.
# ==============================================================================
"""Flower Simulation app."""


from logging import INFO
from typing import Any, Callable, Dict, Optional

try:
import ray
except ImportError:
ray = None # type: ignore

from flwr.client.client import Client
from flwr.common.logger import log
from flwr.server.app import _fl, _init_defaults
from flwr.server.strategy import Strategy
from flwr.simulation.ray_simulation.ray_client_proxy import RayClientProxy

RAY_IMPORT_ERROR: str = """Unable to import module `ray`.

To install the necessary dependencies, install `flwr` with the `simulation` extra:

pip install -U flwr["simulation"]
"""


def start_simulation( # pylint: disable=too-many-arguments
client_fn: Callable[[str], Client],
num_clients: int,
client_resources: Optional[Dict[str, int]] = None,
num_rounds: int = 1,
strategy: Optional[Strategy] = None,
ray_init_args: Optional[Dict[str, Any]] = None,
) -> None:
"""Start a Ray-based Flower simulation server.

Parameters
----------
client_fn : Callable[[str], Client]
A function creating client instances. The function must take a single
str argument called cid. It should return a single client instance
containing the local dataset and model of the client with the matching
client id (i.e., cid).
tanertopal marked this conversation as resolved.
Show resolved Hide resolved
num_clients : int
The total number of clients in this simulation.
client_resources : Optional[Dict[str, int]] (default: None)
CPU and GPU resources for a single client. Supported keys are
`num_cpus` and `num_gpus`. Example: `{"num_cpus": 4, "num_gpus": 1}`
tanertopal marked this conversation as resolved.
Show resolved Hide resolved
num_rounds : int (default: 1)
The number of rounds to train.
strategy : Optional[flwr.server.Strategy] (default: None)
An implementation of the abstract base class `flwr.server.Strategy`. If
no strategy is provided, then `start_server` will use
`flwr.server.strategy.FedAvg`.
ray_init_args : Optional[Dict[str, Any]] (default: None)
Optional dictionary containing `ray.init` arguments.
"""

# Ray cannot be assumed to be installed
if ray is None:
raise ImportError(RAY_IMPORT_ERROR)

# Initialize Ray
if not ray_init_args:
ray_init_args = {}
ray.init(**ray_init_args)
log(
INFO,
"Ray initialized with resources: %s",
ray.cluster_resources(),
)

# Initialize server and server config
config = {"num_rounds": num_rounds}
initialized_server, initialized_config = _init_defaults(None, config, strategy)
log(
INFO,
"Starting Flower simulation running: %s",
initialized_config,
)

# Register one RayClientProxy object for each client with the ClientManager
resources = client_resources if client_resources is not None else {}
for i in range(num_clients):
client_proxy = RayClientProxy(
client_fn=client_fn,
cid=str(i),
resources=resources,
)
initialized_server.client_manager().register(client=client_proxy)

# Start training
_fl(server=initialized_server, config=initialized_config)
15 changes: 15 additions & 0 deletions src/py/flwr/simulation/ray_simulation/__init__.py
@@ -0,0 +1,15 @@
# Copyright 2020 Adap 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.
# ==============================================================================
"""Simulation for Ray-based ClientProxy objects."""
109 changes: 109 additions & 0 deletions src/py/flwr/simulation/ray_simulation/ray_client_proxy.py
@@ -0,0 +1,109 @@
# Copyright 2020 Adap 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.
# ==============================================================================
"""Ray-based Flower ClientProxy implementation."""


from typing import Callable, Dict, Union, cast

try:
import ray
except ImportError:
ray = None # type: ignore

from flwr import common
from flwr.client import Client, NumPyClient
from flwr.client.numpy_client import NumPyClientWrapper
from flwr.server.client_proxy import ClientProxy

ClientFn = Callable[[str], Client]


class RayClientProxy(ClientProxy):
"""Flower client proxy which delegates work using Ray."""

def __init__(self, client_fn: ClientFn, cid: str, resources: Dict[str, int]):
super().__init__(cid)
self.client_fn = client_fn
self.resources = resources

def get_parameters(self) -> common.ParametersRes:
"""Return the current local model parameters."""
future_paramseters_res = launch_and_get_parameters.options(
**self.resources
).remote(self.client_fn, self.cid)
res = ray.get(future_paramseters_res)
return cast(
common.ParametersRes,
res,
)

def fit(self, ins: common.FitIns) -> common.FitRes:
"""Train model parameters on the locally held dataset."""
future_fit_res = launch_and_fit.options(**self.resources).remote(
self.client_fn, self.cid, ins
)
res = ray.get(future_fit_res)
return cast(
common.FitRes,
res,
)

def evaluate(self, ins: common.EvaluateIns) -> common.EvaluateRes:
"""Evaluate model parameters on the locally held dataset."""
future_evaluate_res = launch_and_evaluate.options(**self.resources).remote(
self.client_fn, self.cid, ins
)
res = ray.get(future_evaluate_res)
return cast(
common.EvaluateRes,
res,
)

def reconnect(self, reconnect: common.Reconnect) -> common.Disconnect:
"""Disconnect and (optionally) reconnect later."""
return common.Disconnect(reason="") # Nothing to do here (yet)


@ray.remote # type: ignore
def launch_and_get_parameters(client_fn: ClientFn, cid: str) -> common.ParametersRes:
"""Exectue get_parameters remotely."""
client: Client = _create_client(client_fn, cid)
return client.get_parameters()


@ray.remote # type: ignore
def launch_and_fit(
client_fn: ClientFn, cid: str, fit_ins: common.FitIns
) -> common.FitRes:
"""Exectue fit remotely."""
client: Client = _create_client(client_fn, cid)
return client.fit(fit_ins)


@ray.remote # type: ignore
def launch_and_evaluate(
client_fn: ClientFn, cid: str, evaluate_ins: common.EvaluateIns
) -> common.EvaluateRes:
"""Exectue evaluate remotely."""
client: Client = _create_client(client_fn, cid)
return client.evaluate(evaluate_ins)


def _create_client(client_fn: ClientFn, cid: str) -> Client:
"""Create a client instance."""
client: Union[Client, NumPyClient] = client_fn(cid)
if isinstance(client, NumPyClient):
client = NumPyClientWrapper(numpy_client=client)
return client