Files
2026-07-13 12:40:42 +08:00

368 lines
13 KiB
Python
Executable File

# Copyright (c) 2022 PaddlePaddle Authors. 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.
import abc
import logging
import os
import time
from google.protobuf import text_format
import paddle
from paddle.distributed import fleet
from paddle.distributed.communicator import FLCommunicator
from paddle.distributed.fleet.proto import the_one_ps_pb2
from paddle.distributed.ps.utils.public import is_distributed_env
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt='%(asctime)s %(levelname)-2s [%(filename)s:%(lineno)d] %(message)s'
)
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
class ClientInfoAttr:
CLIENT_ID = 0
DEVICE_TYPE = 1
COMPUTE_CAPACITY = 2
BANDWIDTH = 3
class FLStrategy:
JOIN = 0
WAIT = 1
FINISH = 2
class ClientSelectorBase(abc.ABC):
def __init__(self, fl_clients_info_mp):
self.fl_clients_info_mp = fl_clients_info_mp
self.clients_info = {}
self.fl_strategy = {}
def parse_from_string(self):
if not self.fl_clients_info_mp:
logger.warning("fl-ps > fl_clients_info_mp is null!")
for client_id, info in self.fl_clients_info_mp.items():
self.fl_client_info_desc = the_one_ps_pb2.FLClientInfo()
text_format.Parse(
bytes(info, encoding="utf8"), self.fl_client_info_desc
)
self.clients_info[client_id] = {}
self.clients_info[client_id][ClientInfoAttr.DEVICE_TYPE] = (
self.fl_client_info_desc.device_type
)
self.clients_info[client_id][ClientInfoAttr.COMPUTE_CAPACITY] = (
self.fl_client_info_desc.compute_capacity
)
self.clients_info[client_id][ClientInfoAttr.BANDWIDTH] = (
self.fl_client_info_desc.bandwidth
)
@abc.abstractmethod
def select(self):
pass
class ClientSelector(ClientSelectorBase):
def __init__(self, fl_clients_info_mp):
super().__init__(fl_clients_info_mp)
self.__fl_strategy = {}
def select(self):
self.parse_from_string()
for client_id in self.clients_info:
logger.info(
f"fl-ps > client {client_id} info : {self.clients_info[client_id]}"
)
# ......... to implement ...... #
fl_strategy_desc = the_one_ps_pb2.FLStrategy()
fl_strategy_desc.iteration_num = 99
fl_strategy_desc.client_id = 0
fl_strategy_desc.next_state = "JOIN"
str_msg = text_format.MessageToString(fl_strategy_desc)
self.__fl_strategy[client_id] = str_msg
return self.__fl_strategy
class FLClientBase(abc.ABC):
def __init__(self):
pass
def set_basic_config(self, role_maker, config, metrics):
self.role_maker = role_maker
self.config = config
self.total_train_epoch = int(self.config.get("runner.epochs"))
self.train_statical_info = {}
self.train_statical_info['speed'] = []
self.epoch_idx = 0
self.worker_index = fleet.worker_index()
self.main_program = paddle.static.default_main_program()
self.startup_program = paddle.static.default_startup_program()
self._client_ptr = fleet.get_fl_client()
self._coordinators = self.role_maker._get_coordinator_endpoints()
logger.info(f"fl-ps > coordinator endpoints: {self._coordinators}")
self.strategy_handlers = {}
self.exe = None
self.use_cuda = int(self.config.get("runner.use_gpu"))
self.place = paddle.CUDAPlace(0) if self.use_cuda else paddle.CPUPlace()
self.print_step = int(self.config.get("runner.print_interval"))
self.debug = self.config.get("runner.dataset_debug", False)
self.reader_type = self.config.get("runner.reader_type", "QueueDataset")
self.set_executor()
self.make_save_model_path()
self.set_metrics(metrics)
def set_train_dataset_info(self, train_dataset, train_file_list):
self.train_dataset = train_dataset
self.train_file_list = train_file_list
logger.info(
f"fl-ps > {type(self.train_dataset)}, data_feed_desc:\n {self.train_dataset._desc()}"
)
def set_test_dataset_info(self, test_dataset, test_file_list):
self.test_dataset = test_dataset
self.test_file_list = test_file_list
def set_train_example_num(self, num):
self.train_example_nums = num
def load_dataset(self):
if self.reader_type == "InmemoryDataset":
self.train_dataset.load_into_memory()
def release_dataset(self):
if self.reader_type == "InmemoryDataset":
self.train_dataset.release_memory()
def set_executor(self):
self.exe = paddle.static.Executor(self.place)
def make_save_model_path(self):
self.save_model_path = self.config.get("runner.model_save_path")
if self.save_model_path and (not os.path.exists(self.save_model_path)):
os.makedirs(self.save_model_path)
def set_dump_fields(self):
# DumpField
# TrainerDesc -> SetDumpParamVector -> DumpParam -> DumpWork
if self.config.get("runner.need_dump"):
self.debug = True
dump_fields_path = "{}/epoch_{}".format(
self.config.get("runner.dump_fields_path"), self.epoch_idx
)
dump_fields = self.config.get("runner.dump_fields", [])
dump_param = self.config.get("runner.dump_param", [])
persist_vars_list = self.main_program.all_parameters()
persist_vars_name = [
str(param).split(":")[0].strip().split()[-1]
for param in persist_vars_list
]
logger.info(f"fl-ps > persist_vars_list: {persist_vars_name}")
if dump_fields_path is not None:
self.main_program._fleet_opt['dump_fields_path'] = (
dump_fields_path
)
if dump_fields is not None:
self.main_program._fleet_opt["dump_fields"] = dump_fields
if dump_param is not None:
self.main_program._fleet_opt["dump_param"] = dump_param
def set_metrics(self, metrics):
self.metrics = metrics
self.fetch_vars = [var for _, var in self.metrics.items()]
class FLClient(FLClientBase):
def __init__(self):
super().__init__()
def __build_fl_client_info_desc(self, state_info):
# ......... to implement ...... #
state_info = {
ClientInfoAttr.DEVICE_TYPE: "Android",
ClientInfoAttr.COMPUTE_CAPACITY: 10,
ClientInfoAttr.BANDWIDTH: 100,
}
client_info = the_one_ps_pb2.FLClientInfo()
client_info.device_type = state_info[ClientInfoAttr.DEVICE_TYPE]
client_info.compute_capacity = state_info[
ClientInfoAttr.COMPUTE_CAPACITY
]
client_info.bandwidth = state_info[ClientInfoAttr.BANDWIDTH]
str_msg = text_format.MessageToString(client_info)
return str_msg
def run(self):
self.register_default_handlers()
self.print_program()
self.strategy_handlers['initialize_model_params']()
self.strategy_handlers['init_worker']()
self.load_dataset()
self.train_loop()
self.release_dataset()
self.strategy_handlers['finish']()
def train_loop(self):
while self.epoch_idx < self.total_train_epoch:
logger.info(f"fl-ps > curr epoch idx: {self.epoch_idx}")
self.strategy_handlers['train']()
self.strategy_handlers['save_model']()
self.barrier()
state_info = {
"client id": self.worker_index,
"auc": 0.9,
"epoch": self.epoch_idx,
}
self.push_fl_client_info_sync(state_info)
strategy_dict = self.pull_fl_strategy()
logger.info(f"fl-ps > recved fl strategy: {strategy_dict}")
# ......... to implement ...... #
if strategy_dict['next_state'] == "JOIN":
self.strategy_handlers['infer']()
elif strategy_dict['next_state'] == "FINISH":
self.strategy_handlers['finish']()
def push_fl_client_info_sync(self, state_info):
str_msg = self.__build_fl_client_info_desc(state_info)
self._client_ptr.push_fl_client_info_sync(str_msg)
def pull_fl_strategy(self):
strategy_dict = {}
fl_strategy_str = (
self._client_ptr.pull_fl_strategy()
) # block: wait for coordinator's strategy arrived
logger.info(
f"fl-ps > fl client recved fl_strategy(str):\n{fl_strategy_str}"
)
fl_strategy_desc = the_one_ps_pb2.FLStrategy()
text_format.Parse(
bytes(fl_strategy_str, encoding="utf8"), fl_strategy_desc
)
strategy_dict["next_state"] = fl_strategy_desc.next_state
return strategy_dict
def barrier(self):
fleet.barrier_worker()
def register_handlers(self, strategy_type, callback_func):
self.strategy_handlers[strategy_type] = callback_func
def register_default_handlers(self):
self.register_handlers('train', self.callback_train)
self.register_handlers('infer', self.callback_infer)
self.register_handlers('finish', self.callback_finish)
self.register_handlers(
'initialize_model_params', self.callback_initialize_model_params
)
self.register_handlers('init_worker', self.callback_init_worker)
self.register_handlers('save_model', self.callback_save_model)
def callback_init_worker(self):
fleet.init_worker()
def callback_initialize_model_params(self):
if self.exe is None or self.main_program is None:
raise AssertionError("exe or main_program not set")
self.exe.run(self.startup_program)
def callback_train(self):
epoch_start_time = time.time()
self.set_dump_fields()
fetch_info = [
f"Epoch {self.epoch_idx} Var {var_name}"
for var_name in self.metrics
]
self.exe.train_from_dataset(
program=self.main_program,
dataset=self.train_dataset,
fetch_list=self.fetch_vars,
fetch_info=fetch_info,
print_period=self.print_step,
debug=self.debug,
)
self.epoch_idx += 1
epoch_time = time.time() - epoch_start_time
epoch_speed = self.train_example_nums / epoch_time
self.train_statical_info["speed"].append(epoch_speed)
logger.info("fl-ps > callback_train finished")
def callback_infer(self):
fetch_info = [
f"Epoch {self.epoch_idx} Var {var_name}"
for var_name in self.metrics
]
self.exe.infer_from_dataset(
program=self.main_program,
dataset=self.test_dataset,
fetch_list=self.fetch_vars,
fetch_info=fetch_info,
print_period=self.print_step,
debug=self.debug,
)
def callback_save_model(self):
model_dir = f"{self.save_model_path}/{self.epoch_idx}"
if fleet.is_first_worker() and self.save_model_path:
if is_distributed_env():
fleet.save_persistables(self.exe, model_dir) # save all params
else:
raise ValueError("it is not distributed env")
def callback_finish(self):
fleet.stop_worker()
def print_program(self):
with open(
f"./{self.worker_index}_worker_main_program.prototxt", 'w+'
) as f:
f.write(str(self.main_program))
with open(
f"./{self.worker_index}_worker_startup_program.prototxt",
'w+',
) as f:
f.write(str(self.startup_program))
def print_train_statical_info(self):
with open("./train_statical_info.txt", 'w+') as f:
f.write(str(self.train_statical_info))
class Coordinator:
def __init__(self, ps_hosts):
self._communicator = FLCommunicator(ps_hosts)
self._client_selector = None
def start_coordinator(self, self_endpoint, trainer_endpoints):
self._communicator.start_coordinator(self_endpoint, trainer_endpoints)
def make_fl_strategy(self):
logger.info("fl-ps > running make_fl_strategy(loop) in coordinator\n")
while True:
# 1. get all fl clients reported info
str_map = (
self._communicator.query_fl_clients_info()
) # block: wait for all fl clients info reported
# 2. generate fl strategy
self._client_selector = ClientSelector(str_map)
fl_strategy = self._client_selector.select()
# 3. save fl strategy from python to c++
self._communicator.save_fl_strategy(fl_strategy)
time.sleep(5)