Skip to content

Commit 29ad530

Browse files
authored
✨ Support OpenFL framework in federated-learning-controller (open-cluster-management-io#76)
* add openfl embeded fs Signed-off-by: mrrr61 <mrrr61@outlook.com> * add support for OpenFL framework Signed-off-by: mrrr61 <mrrr61@outlook.com> * remove ustc mirror pypi Signed-off-by: mrrr61 <mrrr61@outlook.com> * use keras 3.11.3 Signed-off-by: mrrr61 <mrrr61@outlook.com> * remove proxy env Signed-off-by: mrrr61 <mrrr61@outlook.com> * move openfl cluster claim samples to openfl examples directory Signed-off-by: mrrr61 <mrrr61@outlook.com> * remove unused placement generation code Signed-off-by: mrrr61 <mrrr61@outlook.com> --------- Signed-off-by: mrrr61 <mrrr61@outlook.com>
1 parent 0e372f7 commit 29ad530

31 files changed

Lines changed: 1063 additions & 37 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.vscode
2+
3+
data/
4+
.vscode
5+
.ipynb_checkpoints
6+
__pycache__
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Variables
2+
REGISTRY ?= quay.io/open-cluster-management
3+
IMAGE_TAG ?= latest
4+
DIST_DIR = dist
5+
APP = openfl-app
6+
PLATFORMS ?= linux/amd64,linux/arm64
7+
IMAGE_FULL_NAME = ${REGISTRY}/${APP}:${IMAGE_TAG}
8+
9+
# Single-arch build (local)
10+
build-app-image:
11+
openssl genrsa -out signer-key.pem -3 3072 && \
12+
cd ${APP} && \
13+
docker build -t ${IMAGE_FULL_NAME} . -f Dockerfile.workspace --secret id=signer-key,src=../signer-key.pem && \
14+
cd ..
15+
16+
# Single-arch push
17+
push-app-image:
18+
docker push ${IMAGE_FULL_NAME}
19+
20+
# Multi-arch build without push (export to local registry/engine not supported directly)
21+
build-platform-image:
22+
cd ${APP} && \
23+
docker buildx build \
24+
--platform ${PLATFORMS} \
25+
--tag ${IMAGE_FULL_NAME} \
26+
--output=type=docker \
27+
. -f Dockerfile && cd ..
28+
29+
# Multi-arch push (requires --push during buildx)
30+
push-platform-image:
31+
cd ${APP} && \
32+
docker buildx build \
33+
--platform ${PLATFORMS} \
34+
--tag ${IMAGE_FULL_NAME} \
35+
--push \
36+
. -f Dockerfile && cd ..
37+
38+
# Clean
39+
clean:
40+
rm -rf $(DIST_DIR) __pycache__ *.spec
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
current_plan_name: default
2+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
# ------------------------------------
4+
# Gramine-ready Workspace Image
5+
# Usage:
6+
# $> docker build . -t openfl-workspace -f Dockerfile.workspace \
7+
# [--build-arg WORKSPACE_PATH=/path/to/workspace] \
8+
# [--secret id=signer-key,src=signer-key.pem]
9+
# ------------------------------------
10+
ARG BASE_IMAGE=ghcr.io/securefederatedai/openfl:latest
11+
FROM ${BASE_IMAGE}
12+
13+
USER root
14+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
15+
16+
WORKDIR /workspace
17+
COPY entrypoint.py /workspace
18+
COPY src/ /workspace/src/
19+
COPY plan/ /workspace/plan/
20+
COPY save/ /workspace/save/
21+
COPY cert/ /workspace/cert/
22+
COPY requirements.txt /workspace/
23+
RUN mkdir /workspace/logs && mkdir /workspace/data && mkdir /workspace/local_state
24+
25+
RUN pip install --no-cache-dir -r ./requirements.txt
26+
27+
# Build enclaves
28+
RUN --mount=type=secret,id=signer-key,dst=/key.pem \
29+
cp -r /opt/venv/lib/python3.10/site-packages/openfl-docker/gramine_app/* /workspace/ && \
30+
make SGX=1 SGX_SIGNER_KEY=/key.pem >> fx.mr_enclave && \
31+
echo "$(cat fx.mr_enclave)" && \
32+
chown -R user /workspace
33+
34+
USER user
35+
ENTRYPOINT ["python", "/workspace/entrypoint.py"]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import argparse
2+
import yaml
3+
from pathlib import Path
4+
import os
5+
import subprocess
6+
7+
PLAN_FILE = Path("plan/plan.yaml")
8+
DATA_FILE = Path("plan/data.yaml")
9+
COLS_FILE = Path("plan/cols.yaml")
10+
11+
12+
def load_plan():
13+
with open(PLAN_FILE, "r") as f:
14+
return yaml.safe_load(f)
15+
16+
17+
def save_plan(config):
18+
with open(PLAN_FILE, "w") as f:
19+
yaml.dump(config, f, sort_keys=False)
20+
21+
22+
def update_server(args):
23+
cfg = load_plan()
24+
25+
if args.cols:
26+
if COLS_FILE.exists():
27+
with open(COLS_FILE, "r") as f:
28+
cols_config = yaml.safe_load(f) or {}
29+
else:
30+
cols_config = {}
31+
32+
cols_list = [col.strip() for col in args.cols.split(',')]
33+
cols_config['collaborators'] = cols_list
34+
35+
with open(COLS_FILE, "w") as f:
36+
yaml.dump(cols_config, f, sort_keys=False)
37+
print(f"[OK] Updated collaborator names in {COLS_FILE}")
38+
cfg["data_loader"]["settings"]["collaborator_count"] = len(cols_list)
39+
40+
cfg["network"]["settings"]["use_tls"] = False
41+
42+
if args.server_ip:
43+
cfg["network"]["settings"]["agg_addr"] = args.server_ip
44+
45+
if args.server_port:
46+
cfg["network"]["settings"]["agg_port"] = args.server_port
47+
48+
if args.num_rounds:
49+
cfg["aggregator"]["settings"]["rounds_to_train"] = int(args.num_rounds)
50+
51+
if args.model_dir:
52+
best_file = os.path.basename(cfg["aggregator"]["settings"]["best_state_path"])
53+
last_file = os.path.basename(cfg["aggregator"]["settings"]["last_state_path"])
54+
cfg["aggregator"]["settings"]["best_state_path"] = os.path.join(args.model_dir, best_file)
55+
cfg["aggregator"]["settings"]["last_state_path"] = os.path.join(args.model_dir, last_file)
56+
57+
save_plan(cfg)
58+
print(f"[OK] Updated server settings in {PLAN_FILE}")
59+
60+
os.execvp("fx", ["fx", "aggregator", "start"])
61+
62+
63+
def update_client(args):
64+
with open(DATA_FILE, "a") as f:
65+
f.write(f"{args.name},{args.data_path}\n")
66+
67+
cfg = load_plan()
68+
cfg["network"]["settings"]["use_tls"] = False
69+
70+
if args.server_ip:
71+
cfg["network"]["settings"]["agg_addr"] = args.server_ip
72+
73+
if args.server_port:
74+
cfg["network"]["settings"]["agg_port"] = args.server_port
75+
76+
if args.num_rounds:
77+
cfg["aggregator"]["settings"]["rounds_to_train"] = int(args.num_rounds)
78+
79+
if args.model_dir:
80+
best_file = os.path.basename(cfg["aggregator"]["settings"]["best_state_path"])
81+
last_file = os.path.basename(cfg["aggregator"]["settings"]["last_state_path"])
82+
cfg["aggregator"]["settings"]["best_state_path"] = os.path.join(args.model_dir, best_file)
83+
cfg["aggregator"]["settings"]["last_state_path"] = os.path.join(args.model_dir, last_file)
84+
85+
save_plan(cfg)
86+
print(f"[OK] Updated client settings in {PLAN_FILE}")
87+
88+
os.execvp("fx", ["fx", "collaborator", "start", "-n", args.name])
89+
90+
91+
def main():
92+
parser = argparse.ArgumentParser(description="Update OpenFL plan.yaml")
93+
94+
subparsers = parser.add_subparsers(dest="role", required=True)
95+
96+
# docker run --rm image server --server-port 8080 --num-rounds 3 --cols client1,client2 --model-dir models
97+
sp_server = subparsers.add_parser("server", help="Update server (aggregator) settings")
98+
sp_server.add_argument("--server-ip", help="Aggregator IP address")
99+
sp_server.add_argument("--server-port", type=int, help="Aggregator port")
100+
sp_server.add_argument("--num-rounds", type=int, help="Number of rounds to train")
101+
sp_server.add_argument("--cols", help="Comma-separated list of collaborator names for cols.yaml")
102+
sp_server.add_argument("--model-dir", help="Directory for model files")
103+
sp_server.set_defaults(func=update_server)
104+
105+
# docker run --rm image client --name client1 --data-path /data/client1 --server-ip 172.17.0.2 --server-port 8080 --num-rounds 3 --model-dir models
106+
sp_client = subparsers.add_parser("client", help="Update client (collaborator) settings")
107+
sp_client.add_argument("--name", required=True, help="Collaborator name")
108+
sp_client.add_argument("--data-path", required=True, help="Path to collaborator data")
109+
sp_client.add_argument("--server-ip", help="Aggregator IP address")
110+
sp_client.add_argument("--server-port", type=int, help="Aggregator port")
111+
sp_client.add_argument("--num-rounds", type=int, help="Number of rounds to train")
112+
sp_client.add_argument("--model-dir", help="Directory for model files")
113+
sp_client.set_defaults(func=update_client)
114+
115+
args = parser.parse_args()
116+
args.func(args)
117+
118+
119+
if __name__ == "__main__":
120+
main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (C) 2020-2021 Intel Corporation
2+
# Licensed subject to the terms of the separately executed evaluation license agreement between Intel Corporation and you.
3+
4+
collaborators:
5+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (C) 2020-2021 Intel Corporation
2+
# Licensed subject to the terms of the separately executed evaluation license agreement between Intel Corporation and you.
3+
4+
# collaborator_name,data_directory_path
5+
one,1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
../../workspace/plan/defaults
2+
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
aggregator:
2+
settings:
3+
best_state_path: 6677/best.pbuf
4+
db_store_rounds: 2
5+
init_state_path: save/init.pbuf
6+
last_state_path: 6677/last.pbuf
7+
persist_checkpoint: true
8+
persistent_db_path: local_state/tensor.db
9+
rounds_to_train: 10
10+
template: openfl.component.Aggregator
11+
assigner:
12+
settings:
13+
task_groups:
14+
- name: learning
15+
percentage: 1.0
16+
tasks:
17+
- aggregated_model_validation
18+
- train
19+
- locally_tuned_model_validation
20+
- name: evaluation
21+
percentage: 0
22+
tasks:
23+
- aggregated_model_validation
24+
template: openfl.component.RandomGroupedAssigner
25+
collaborator:
26+
settings:
27+
db_store_rounds: 1
28+
opt_treatment: RESET
29+
use_delta_updates: false
30+
template: openfl.component.Collaborator
31+
compression_pipeline:
32+
settings: {}
33+
template: openfl.pipelines.NoCompressionPipeline
34+
data_loader:
35+
settings:
36+
batch_size: 256
37+
collaborator_count: 2
38+
data_group_name: mnist
39+
template: src.dataloader.KerasMNISTInMemory
40+
network:
41+
settings:
42+
agg_addr: 172.16.1.11
43+
agg_port: 58112
44+
cert_folder: cert
45+
client_reconnect_interval: 5
46+
enable_atomic_connections: false
47+
hash_salt: auto
48+
require_client_auth: true
49+
transport_protocol: grpc
50+
use_tls: false
51+
template: openfl.federation.Network
52+
task_runner:
53+
settings: {}
54+
template: src.taskrunner.KerasCNN
55+
tasks:
56+
aggregated_model_validation:
57+
function: validate_task
58+
kwargs:
59+
apply: global
60+
batch_size: 32
61+
metrics:
62+
- accuracy
63+
locally_tuned_model_validation:
64+
function: validate_task
65+
kwargs:
66+
apply: local
67+
batch_size: 32
68+
metrics:
69+
- accuracy
70+
settings: {}
71+
train:
72+
function: train_task
73+
kwargs:
74+
batch_size: 32
75+
epochs: 1
76+
metrics:
77+
- loss
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
keras==3.11.3
2+
tensorflow==2.18.0

0 commit comments

Comments
 (0)