chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,36 @@
# 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.
__all__ = []
from .collective import CollectiveController, CollectiveElasticController
from .ipu_controller import IPUController
from .ps import PSController
from .rpc import RpcController
# the order is extremely important
_controllers = [
IPUController,
CollectiveElasticController,
PSController,
RpcController,
CollectiveController,
]
def init(ctx):
for c in _controllers:
if c.enable(ctx):
ctx.print()
return c(ctx)
@@ -0,0 +1,323 @@
# 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 json
import os
from ..context.device import DeviceType
from .controller import Controller, ControllerMode
class CollectiveController(Controller):
def __init__(self, ctx):
self._tuner_run_mode = None # 'tuner_only', 'run_only', 'tuner_and_run'
super().__init__(ctx)
@classmethod
def enable(cls, ctx):
# collective is the default mode
if ctx:
ctx.logger.debug(f"{cls.__name__} enabled")
ctx.args.run_mode = ControllerMode.COLLECTIVE
return True
else:
return False
def build_pod(self):
skip_run = self._build_pod_with_tuner()
if skip_run:
return
if (
self.ctx.args.master is None
and self.ctx.args.start_port
and self.ctx.args.ips
):
return self._build_pod_with_args()
else:
if self.ctx.args.auto_parallel_config is None:
skip_run = True
# only when skip_run is False, should not reset pod
return self._build_pod_with_master(skip_run)
def _build_pod_with_tuner(self):
auto_parallel_config = self.ctx.args.auto_parallel_config
if auto_parallel_config is not None:
if not os.path.exists(auto_parallel_config):
self.ctx.logger.warning("auto_parallel_conf not exists!")
if not auto_parallel_config.endswith(".json"):
self.ctx.logger.warning(
"auto_parallel_config should be a json format file!"
)
with open(auto_parallel_config, 'r') as robj:
auto_parallel_data = json.loads(robj.read())
self._tuner_run_mode = auto_parallel_data.get(
"tuner_run_mode", 'tuner_and_run'
)
self.ctx.logger.info(f"tuner_run_mode is: {self._tuner_run_mode}")
endpoint = f"127.0.0.1:{self.ctx.node.get_free_port()}"
pod_replicas = self.pod_replicas()
if self._tuner_run_mode in ['tuner_only', 'tuner_and_run']:
e = {
"PADDLE_AUTO_PARALLEL_CONFIG": self.ctx.args.auto_parallel_config,
"PADDLE_TRAINERS_NUM": "1",
"PADDLE_TRAINER_ENDPOINTS": endpoint,
"PADDLE_TRAINER_ID": "0",
"PADDLE_CURRENT_ENDPOINT": endpoint,
"FLAGS_selected_gpus": "0",
"PADDLE_AUTO_PARALLEL_STAGE": "tuner",
"PADDLE_GLOBAL_SIZE": f"{pod_replicas * int(self.ctx.args.nnodes)}",
"PADDLE_LOCAL_SIZE": f"{pod_replicas}",
}
log_file = "tuner.log"
self.add_container(envs=e, log_file=log_file, is_init=True)
if self._tuner_run_mode == 'tuner_only':
return True
return False
def _build_pod_with_args(self):
self.pod.replicas = self.pod_replicas()
start_port = int(self.ctx.args.start_port)
ips = self.ctx.args.ips.split(',')
job_endpoints = [
f"{h}:{p + start_port}"
for h in ips
for p in range(self.pod.replicas)
]
self.ctx.logger.debug(f"job endpoints: {job_endpoints}")
self.ctx.logger.warning(
f"master is set by args, it will be overwritten by {job_endpoints[0]}."
)
# this is necessary for tcp store to work when endpoints cannot be passed to sub processes.
self.ctx.args.master = job_endpoints[0]
rank_offset = (
ips.index(self.ctx.node.ip) * self.pod.replicas
if self.ctx.node.ip in ips
else 0
)
self.save_pod_log(job_endpoints)
selected_dev_key = self.ctx.node.device.get_selected_device_key()
selected_dev_list = self.ctx.node.device.get_selected_devices(
self.ctx.args.devices
)
for i in range(self.pod.replicas):
e = {
"PADDLE_MASTER": self.ctx.args.master,
"PADDLE_GLOBAL_SIZE": f"{len(job_endpoints)}",
"PADDLE_LOCAL_SIZE": f"{self.pod.replicas}",
"PADDLE_GLOBAL_RANK": f"{i + rank_offset}",
"PADDLE_LOCAL_RANK": f"{i}",
"PADDLE_NNODES": f"{len(ips)}",
# compatible env
"PADDLE_CURRENT_ENDPOINT": job_endpoints[i + rank_offset],
"PADDLE_TRAINER_ID": f"{i + rank_offset}",
"PADDLE_TRAINERS_NUM": f"{len(job_endpoints)}",
"PADDLE_RANK_IN_NODE": str(i),
"PADDLE_AUTO_CLUSTER": str(self.ctx.args.auto_cluster_config),
}
e.update({"PADDLE_TRAINER_ENDPOINTS": ",".join(job_endpoints)})
if self._tuner_run_mode is not None:
e.update(
{
"PADDLE_AUTO_PARALLEL_CONFIG": self.ctx.args.auto_parallel_config,
"PADDLE_AUTO_PARALLEL_STAGE": "run",
}
)
if len(selected_dev_list) > 0:
if self.ctx.node.device.dtype == DeviceType.CUSTOM_DEVICE:
e.update(self.ctx.node.device.get_custom_device_envs())
if self.pod.replicas == 1:
e.update({selected_dev_key: ",".join(selected_dev_list)})
else:
e.update({selected_dev_key: selected_dev_list[i]})
else:
e.update({'PADDLE_DISTRI_BACKEND': 'gloo'})
log_file = f"workerlog.{i}"
self.add_container(envs=e, log_file=log_file)
return True
def _build_pod_with_master(self, reset_pod=True):
self.pod.replicas = self.pod_replicas()
# rank will be reset when restart
self.pod.rank = int(self.ctx.args.rank)
port = self.ctx.node.get_free_port()
# compatible
endpoints = [
f"{self.ctx.node.ip}:{p}"
for p in self.ctx.node.get_free_ports(
self.pod.replicas, self.pod.rank
)
]
data = json.dumps(
{
'name': self.pod.name,
'rank': self.pod.rank,
'replicas': self.pod.replicas,
'dtype': self.ctx.node.device.dtype,
'candidate': f'{self.ctx.node.ip}:{port}',
'endpoints': ",".join(endpoints),
}
)
peer_list, rank = self.master.sync_peers(
f'/{self.job.id}/info',
self.pod.name,
data,
self.job.replicas,
self.pod.rank,
)
self.pod.rank = rank
if len(peer_list) < 1:
return False
peer_list = [json.loads(i) for i in peer_list]
self.ctx.logger.debug(f"sync peers done {peer_list}")
self.save_pod_log(peer_list)
global_size = sum([i['replicas'] for i in peer_list])
rank_offset = sum([i['replicas'] for i in peer_list[:rank]])
'''
The new designed collective need nothing but a master endpoint
'''
collective_master = peer_list[0]['candidate']
# get collective master ip
collective_master_ip = collective_master.split(':')[0].strip()
os.environ["COLLECTIVE_MASTER_IP"] = collective_master_ip
job_endpoints = [i['endpoints'] for i in peer_list]
if reset_pod:
self.pod.reset()
selected_dev_key = self.ctx.node.device.get_selected_device_key()
selected_dev_list = self.ctx.node.device.get_selected_devices(
self.ctx.args.devices
)
for i in range(self.pod.replicas):
e = {
"PADDLE_MASTER": collective_master,
"PADDLE_GLOBAL_SIZE": f"{global_size}",
"PADDLE_LOCAL_SIZE": f"{self.pod.replicas}",
"PADDLE_GLOBAL_RANK": f"{i + rank_offset}",
"PADDLE_LOCAL_RANK": f"{i}",
"PADDLE_NNODES": f"{self.job.replicas}",
# compatible env
"PADDLE_CURRENT_ENDPOINT": endpoints[i],
"PADDLE_TRAINER_ID": f"{i + rank_offset}",
"PADDLE_TRAINERS_NUM": f"{global_size}",
"PADDLE_RANK_IN_NODE": str(i),
"PADDLE_AUTO_CLUSTER": str(self.ctx.args.auto_cluster_config),
}
e.update({"PADDLE_TRAINER_ENDPOINTS": ",".join(job_endpoints)})
if self._tuner_run_mode is not None:
e.update(
{
"PADDLE_AUTO_PARALLEL_CONFIG": self.ctx.args.auto_parallel_config,
"PADDLE_AUTO_PARALLEL_STAGE": "run",
}
)
if len(selected_dev_list) > 0:
if self.ctx.node.device.dtype == DeviceType.CUSTOM_DEVICE:
e.update(self.ctx.node.device.get_custom_device_envs())
if self.pod.replicas == 1:
e.update({selected_dev_key: ",".join(selected_dev_list)})
else:
e.update({selected_dev_key: selected_dev_list[i]})
else:
e.update({'PADDLE_DISTRI_BACKEND': 'gloo'})
# log_file = "{}.{}.{}.log".format(self.job.id, self.pod.name, i)
log_file = f"workerlog.{i}"
self.add_container(envs=e, log_file=log_file)
return True
class CollectiveElasticController(CollectiveController):
@classmethod
def enable(cls, ctx):
if ctx.args.master and ctx.args.master.startswith("etcd://"):
ctx.logger.debug(f"{cls.__name__} enabled")
ctx.args.run_mode = ControllerMode.COLLECTIVE
return True
else:
return False
def register(self):
if self.job.id == 'default':
self.ctx.logger.warning(
'Using default job name may cause conflict, add --job_id in args'
)
self.master.register_heartbeat(self.job.id, self.pod.name)
def run(self):
timeout = int(self.ctx.args.elastic_timeout)
timeout = timeout if self.job.elastic else timeout * 10
self.register()
while self.pod.restart <= self.ctx.args.max_restart:
self.build_job()
self.ctx.logger.info("Waiting peer ready...")
ok, replicas = self.master.wait_peer_ready(
self.job.replicas_min, self.job.replicas_max, timeout
)
if ok:
self.job.replicas = replicas
else:
self.ctx.logger.warning(f"peer not ready {self.job}")
if self.ctx.is_auto_tuner_mode():
self.ctx.logger.info(
"Failed to start peer, auto tuner exit."
)
import sys
sys.exit(-1)
break
self.ctx.logger.debug(f"Run {self.job}")
if not self.build_pod():
continue
self.master.set_status(self.ctx.status.RUNNING)
self.deploy_pod()
if self.watch():
break
self.ctx.logger.debug(f"Job done {self.job}")
@@ -0,0 +1,341 @@
# 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 copy
import os
import signal
import sys
from paddle.distributed.launch.job.container import Container
from paddle.distributed.launch.job.job import Job
from paddle.distributed.launch.job.pod import Pod
from .master import Master
from .watcher import Watcher
class ControllerMode:
COLLECTIVE = "collective"
PS = "ps"
IPU = "ipu"
RPC = "rpc"
class ControllerBase:
def __init__(self, ctx):
signal.signal(signal.SIGTERM, self.signal_handler)
signal.signal(signal.SIGABRT, self.signal_handler)
signal.signal(signal.SIGINT, self.signal_handler)
if ctx.is_auto_tuner_mode():
if not ctx.run_best:
# set per task timeout
signal.signal(signal.SIGALRM, self.not_exit_signal_handler)
signal.alarm(ctx.max_time_per_task)
else:
signal.alarm(0)
self.ctx = ctx
self.master = Master.factory(self.ctx)
self.watcher = Watcher(self.ctx)
self.job = Job(
nnodes=self.ctx.args.nnodes,
mode=self.ctx.args.run_mode,
jid=self.ctx.args.job_id,
)
self.pod = Pod()
self.ctx.set_envs({"POD_NAME": self.pod.name})
self.join_server = None
def deploy_pod(self):
assert len(self.pod.containers) + len(self.pod.init_containers) > 0, (
"No container in the pod"
)
self.ctx.logger.info(f"Run {self.pod}")
if len(self.pod.init_containers) > 0:
self.ctx.logger.debug(self.pod.init_containers[0])
if len(self.pod.containers) > 0:
self.ctx.logger.debug(self.pod.containers[0])
self.save_pod_env()
self.ctx.status.run()
self.pod.deploy()
def run(self):
self.build_job()
self.build_pod()
self.deploy_pod()
self.watch()
def watch(self) -> bool:
'''
watch self and peer status, return true to exit
'''
# TODO(kuizhiqing) unify ctx.status and master status
self.ctx.logger.info(f"Watching {self.pod}")
while not self.ctx.status.is_done():
status = self.pod.watch(timeout=2)
# if self.ctx.continuous_log():
# default to print log
self.pod.logs()
# completed
if status == self.ctx.status.COMPLETED:
self.ctx.status.complete()
self.master.set_status(status)
while self.pod.logs():
pass
self.ctx.logger.info(f"Pod {status}")
return True
# self failure
elif status == self.ctx.status.FAILED:
self.ctx.status.fail()
self.master.set_status(status)
self.master.restart_peer()
fc = self.pod.failed_container()
self.ctx.logger.info(f"Pod {status}")
self.ctx.logger.error(f"Container failed !!!\n{fc[0]}")
self.ctx.logger.info(
"------------------------- ERROR LOG DETAIL -------------------------"
)
fc[0].tail()
if self.ctx.args.elastic_level <= 0:
self.pod.stop(timeout=3)
return True
else:
self.pod.stop(timeout=30)
return False
# peer failure
if (
self.ctx.status.is_restarting()
and self.master.get_status() != self.ctx.status.COMPLETED
):
# when peer failure, stop peer
if self.ctx.args.elastic_level == -1:
self.pod.stop(timeout=3)
return True
self.pod.stop(timeout=30)
return False
def stop(self, sigint=None):
self.ctx.logger.debug("Controller stop")
self.watcher.stop()
self.master.stop()
self.pod.stop(timeout=30)
def finalize(self, exit=True):
self.pod.join()
self.master.stop()
self.ctx.logger.info(f"Exit code {self.pod.exit_code}")
if exit:
sys.exit(self.pod.exit_code)
def signal_handler(self, sigint, frame):
if hasattr(self, 'sigint'):
self.ctx.logger.info("Force quit in 10 seconds...")
self.pod.stop(timeout=10)
sys.exit(sigint)
self.ctx.logger.info(f"Terminating with signal {sigint}")
self.sigint = sigint
self.ctx.status.done()
self.stop(sigint=sigint)
self.ctx.logger.info(f"Exit with signal {sigint}")
sys.exit(sigint)
def not_exit_signal_handler(self, sigint, frame):
if hasattr(self, 'sigint'):
self.ctx.logger.info("Force quit in 10 seconds...")
self.pod.stop(timeout=10)
self.ctx.logger.info(f"Terminating with signal {sigint}")
self.sigint = sigint
self.ctx.status.done()
self.stop(sigint=sigint)
self.ctx.logger.info(f"Exit with signal {sigint}")
class Controller(ControllerBase):
'''
Controller API for customization
'''
def build_job(self):
'''
build job fill the job info.
'''
self.ctx.logger.info(self.job)
def build_pod(self) -> bool:
'''
build pod includes creating containers etc.
Return True if succeed
'''
raise NotImplementedError
def _get_entrypoint(self):
if self.ctx.args.training_script.endswith('.py'):
if os.environ.get("WITH_COVERAGE") == "ON":
entrypoint = [
sys.executable,
"-u",
"-m",
"coverage",
"run",
"--branch",
"-p",
self.ctx.args.training_script,
]
else:
entrypoint = [
sys.executable,
"-u",
self.ctx.args.training_script,
]
elif self.ctx.args.training_script.endswith('.pyxes'):
entrypoint = [sys.executable, self.ctx.args.training_script]
else:
entrypoint = [self.ctx.args.training_script]
entrypoint.extend(self.ctx.args.training_script_args)
return entrypoint
def _get_out_err_file(self, out=None, err=None):
if out and self.ctx.args.log_dir != "":
out = os.path.join(self.ctx.args.log_dir, out)
if err and self.ctx.args.log_dir != "":
err = os.path.join(self.ctx.args.log_dir, err)
return out, (err or out)
def new_container(
self, entrypoint=None, envs={}, use_ctx_env=True, out=None, err=None
):
c = Container(
entrypoint=(entrypoint or self._get_entrypoint()),
env=(self.ctx.get_envs() if use_ctx_env else {}),
overwrite_log=self.ctx.args.log_overwrite,
)
c.outfile, c.errfile = self._get_out_err_file(out, err)
c.update_env(envs)
return c
def add_container(
self,
container=None,
entrypoint=None,
envs={},
log_file=None,
is_init=False,
):
if not container:
envs = copy.deepcopy(envs)
envs['PADDLE_LOG_DIR'] = str(os.path.abspath(self.ctx.args.log_dir))
container = self.new_container(
entrypoint=entrypoint, envs=envs, out=log_file, err=log_file
)
if is_init:
self.pod.add_init_container(container)
else:
self.pod.add_container(container)
def pod_replicas(self):
'''
how many process/container should be run in pod
'''
if self.ctx.args.nproc_per_node:
return int(self.ctx.args.nproc_per_node)
elif self.ctx.args.devices:
return len(self.ctx.args.devices.split(','))
else:
return self.ctx.node.device.count
def save_pod_log(self, info):
'''
save_pod_log append *info* to the log file of pod.name
'''
if not self.ctx.args.log_dir:
return
f = os.path.join(
self.ctx.args.log_dir,
f'{self.job.id}.{self.pod.name}.log',
)
try:
os.makedirs(os.path.dirname(f), exist_ok=True)
with open(f, 'a+') as fd:
if fd.tell() == 0:
fd.write(str(os.environ))
fd.write("\n")
fd.write(str(info))
fd.write("\n")
except Exception as e:
self.ctx.logger.error(f"save log failed because {e}")
def save_pod_env(self):
assert len(self.pod.containers) + len(self.pod.init_containers) > 0, (
"No container in the pod"
)
if not self.ctx.args.log_dir:
return
for c in self.pod.init_containers:
self._save_container_env(c, is_init=True)
for c in self.pod.containers:
self._save_container_env(c)
def _save_container_env(self, container, is_init=False):
f = os.path.join(
self.ctx.args.log_dir,
(
f'envlog.init.{container.rank}'
if is_init
else f'envlog.{container.rank}'
),
)
try:
os.makedirs(os.path.dirname(f), exist_ok=True)
with open(f, container.log_mode) as fd:
fd.writelines(
f"{k}={v}\n" for k, v in sorted(container.env.items())
)
except Exception as e:
self.ctx.logger.error(f"save pod env log failed because {e}")
@@ -0,0 +1,178 @@
# 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 argparse
import os
import sys
from paddle.distributed.launch.job.container import Container
from .collective import CollectiveController, ControllerMode
class IPUController(CollectiveController):
@classmethod
def enable(cls, ctx):
if ctx.args.training_script == "ipu":
ctx.logger.debug(f"{cls.__name__} enabled")
ctx.args.run_mode = ControllerMode.IPU
return True
else:
return False
def parse_ipu_args(self, args_list):
parser = argparse.ArgumentParser()
parser.add_argument(
"--hosts", type=str, help="The hosts for IPU distributed training."
)
parser.add_argument(
"--nproc_per_host",
type=int,
help="The number of processes launched per host.",
)
parser.add_argument(
"--ipus_per_replica",
type=int,
help="The number of IPUs requested per replica.",
)
parser.add_argument(
"--ipu_partition",
type=str,
help="The partition name of IPU devices.",
)
parser.add_argument(
"--vipu_server", type=str, help="The ip of the IPU device manager."
)
parser.add_argument(
"training_script",
type=str,
help="The full path to the IPU distributed training program/script to be launched in parallel. e.g., ``training.py``.",
)
parser.add_argument('training_script_args', nargs=argparse.REMAINDER)
return parser.parse_args(args_list)
def replace_training_script(self):
# IPU distributed computing is based on PopRun which is a wrapper of MPI.
self.ctx.args.training_script = "poprun"
poprun_args = self.parse_ipu_args(self.ctx.args.training_script_args)
num_ipus = int(self.ctx.args.devices)
# The number of replicas for data parallel
assert (num_ipus % poprun_args.ipus_per_replica) == 0, (
f"The number of IPUs:{num_ipus} mod the number of IPUs per replica:{poprun_args.ipus_per_replica} must == 0"
)
num_replicas = num_ipus // poprun_args.ipus_per_replica
self.ctx.logger.info(f"The number of total replicas is {num_replicas}.")
# The number of processes
num_nodes = len(poprun_args.hosts.split(','))
num_procs = num_nodes * poprun_args.nproc_per_host
self.ctx.logger.info(f"The number of total processes is {num_procs}.")
assert (num_replicas % num_procs) == 0, (
f"The number of replicas:{num_replicas} mod the number of processes:{num_procs} must == 0"
)
# hosts and endpoints
hosts = poprun_args.hosts.replace(' ', '').split(',')
endpoints = [x + ":8090" for x in hosts]
# args for poprun
poprun_command = []
poprun_command.append(f'--num-instances={num_procs}')
poprun_command.append(f'--num-replicas={num_replicas}')
poprun_command.append(
f'--ipus-per-replica={poprun_args.ipus_per_replica}'
)
poprun_command.append('--host={}'.format(','.join(hosts)))
poprun_command.append(f'--vipu-partition={poprun_args.ipu_partition}')
poprun_command.append(f'--vipu-server-host={poprun_args.vipu_server}')
poprun_command.extend(
[
'--update-partition=no',
'--vipu-server-timeout=120',
'--print-topology=yes',
'--numa-aware=yes',
]
)
# global envs
global_envs = '--mpi-local-args=\''
log_level = os.getenv('POPART_LOG_LEVEL', None)
if log_level:
global_envs += f'-x POPART_LOG_LEVEL={log_level} '
global_envs += (
'-x PADDLE_TRAINERS_NUM={} -x PADDLE_TRAINER_ENDPOINTS={}'.format(
num_procs, ','.join(endpoints)
)
)
global_envs += '\''
poprun_command.append(global_envs)
# local envs
for idx in range(num_procs):
cur_endpoint = endpoints[idx // poprun_args.nproc_per_host]
rank_in_node = idx % poprun_args.nproc_per_host
poprun_command.append(
f'--instance-mpi-local-args={idx}:"-x PADDLE_TRAINER_ID={idx} -x PADDLE_CURRENT_ENDPOINT={cur_endpoint} -x PADDLE_RANK_IN_NODE={rank_in_node}"'
)
# executor
poprun_command.append(sys.executable)
# script and script args
poprun_command.append(poprun_args.training_script)
poprun_command.extend(poprun_args.training_script_args)
# for debug
print("----------- PopRun Command -----------")
print("poprun \\")
for i in range(len(poprun_command) - 1):
print(f"{poprun_command[i]} \\")
print(f"{poprun_command[len(poprun_command) - 1]}")
print("---------------------------------------")
# replace training_script_args
self.ctx.args.training_script_args = poprun_command
def _get_entrypoint(self):
entrypoint = [self.ctx.args.training_script]
entrypoint.extend(self.ctx.args.training_script_args)
entrypoint = [" ".join(entrypoint)]
return entrypoint
def new_container(
self, entrypoint=None, envs={}, use_ctx_env=True, out=None, err=None
):
c = Container(
entrypoint=(entrypoint or self._get_entrypoint()),
env=(self.ctx.get_envs() if use_ctx_env else {}),
)
c.outfile, c.errfile = self._get_out_err_file(out, err)
c.update_env(envs)
# Need subprocess.Popen(shell=True) for PopRun command
c.shell = True
return c
def run(self):
# Replace the training script with the PopRun command
self.replace_training_script()
self.build_job()
self.build_pod()
self.deploy_pod()
self.watch()
@@ -0,0 +1,344 @@
# 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 copy
import ipaddress
import json
import random
import sys
import threading
import time
from paddle.distributed.launch.utils.kv_client import KVClient
from paddle.distributed.launch.utils.kv_server import KVServer
ETCD_PROTOCOL = 'etcd://'
def _cmp_by_ip(x):
x = json.loads(x)
ip_x = x.get('candidate', '127.0.0.1:8080').split(':')[0]
return int(ipaddress.IPv4Address(ip_x))
class Master:
'''
Master is a distributed store design to exchange info among nodes
'''
MAIN = "main"
STANDBY = "standby"
PARTICIPANT = "participant"
def __init__(self, ctx):
self.ctx = ctx
self.server = None
self.initialized = False
self.endpoint = None
def stop(self):
raise NotImplementedError
def set_status(self, status):
pass
def get_status(self):
return None
def restart_peer(self):
pass
def sync_peers(self, prefix, key, value, size, rank=-1) -> (list, int):
raise NotImplementedError
@classmethod
def factory(cls, ctx):
if ctx.args.master and ctx.args.master.startswith(ETCD_PROTOCOL):
return ETCDMaster(ctx)
else:
return HTTPMaster(ctx)
class HTTPMaster(Master):
def lazy_init(self):
if self.initialized:
return
self.role = Master.PARTICIPANT
if self.ctx.args.master:
self.endpoint = self.ctx.args.master
ip, port = self.endpoint.split(':')
if ip in ['127.0.0.1', self.ctx.node.ip]:
time.sleep(2 * random.random())
while not self.ctx.node.is_server_ready(ip, int(port)):
try:
self.server = KVServer(int(port))
self.role = Master.MAIN
break
except Exception as e:
self.ctx.logger.warning(f"start master failed {e}")
time.sleep(0.1)
continue
else:
port = self.ctx.node.get_free_port()
self.endpoint = f"{self.ctx.node.ip}:{port}"
self.server = KVServer(port)
self.role = Master.MAIN
print("Copy the following command to other nodes to run.")
cmd = [
sys.executable.split('/')[-1],
"-m",
"paddle.distributed.launch",
]
cmd.extend(["--master", self.endpoint])
cmd.extend(sys.argv[1:])
print("-" * 80)
print(" ".join(cmd))
print("-" * 80)
if int(self.ctx.args.rank) >= 0:
self.ctx.logger.warning(
"--rank set in the command may not compatible in auto mode"
)
if '127.0.0.1' in self.endpoint:
self.endpoint = self.endpoint.replace('127.0.0.1', self.ctx.node.ip)
self.client = KVClient(self.endpoint)
self.initialized = True
self._start_server()
def _start_server(self):
if self.server and not self.server.started:
self.server.start()
self.ctx.logger.debug(f"KV server start at {self.endpoint}")
def _stop_server(self):
if self.server and not self.server.stopped:
self.server.stop()
self.ctx.logger.debug("KV server stopped")
def stop(self):
self._stop_server()
def sync_peers(self, prefix, key, value, size, rank=-1) -> (list, int):
if size < 2:
return [value], 0
self.ctx.logger.info("Waiting peer start...")
self.lazy_init()
while not self.ctx.status.is_done():
if self.client.wait_server_ready(timeout=5):
break
else:
self.ctx.logger.warning("master not ready")
time.sleep(0.1)
# 'aaaaaa' make sure main pod (master server) as rank 0
ky = 'aaaaaa' if rank < 0 and self.role == Master.MAIN else key
k = f"{prefix}/{ky}/{rank}"
while not self.ctx.status.is_done():
if not self.client.put(k, value):
self.ctx.logger.warning("put value failed")
time.sleep(0.1)
continue
rjson = self.client.get_prefix(prefix)
self.ctx.logger.debug(f"sync peers {rjson}")
if rjson and len(rjson) == size:
if self.ctx.args.sort_ip:
ret = sorted(rjson.values(), key=_cmp_by_ip)
idx = ret.index(value)
return ret, idx
elif rank < 0:
keys = list(rjson.keys())
keys.sort()
ret = [rjson[k] for k in keys]
idx = ret.index(value)
return ret, idx
else:
ret = [None] * size
for k, v in rjson.items():
ret[int(k.split('/')[-1])] = v
return ret, rank
else:
time.sleep(0.5)
return [], 0
class ETCDMaster(Master):
def __init__(self, ctx):
super().__init__(ctx)
if self.ctx.args.master:
# etcd://localhost:2379
self.endpoint = self.ctx.args.master.removeprefix("etcd://")
import etcd3
from ..utils.etcd_client import ETCDClient
host, port = self.endpoint.split(':')
if ctx.is_auto_tuner_mode():
self.client = ETCDClient(host=host, port=port)
else:
self.client = etcd3.client(host=host, port=port)
def sync_peers(self, prefix, key, value, size, rank=-1) -> (list, int):
'''
sync_peers gather all value for key under scope prefix
result always be sorted either by rank or alphabet of pod.name
'''
if size < 2:
return [value], 0
self.ctx.logger.info("Waiting peer start...")
path = f"{prefix}/{key}/{rank}"
self.client.delete_prefix(prefix)
self.ctx.logger.debug(f"sync path {path} value {value}")
while not self.ctx.status.is_done():
self.client.put(path, value.encode('latin-1'))
result = list(self.client.get_prefix(prefix))
result = copy.deepcopy(result)
self.ctx.logger.debug(f"sync peers {result}")
if len(result) == size:
if self.ctx.args.sort_ip:
values = [i[0].decode() for i in result]
ret = sorted(values, key=_cmp_by_ip)
idx = ret.index(value)
return ret, idx
elif rank < 0:
keys = [i[1].key.decode() for i in result]
sorted_keys = [i[1].key.decode() for i in result]
sorted_keys.sort()
values = [i[0].decode() for i in result]
ret = [values[keys.index(k)] for k in sorted_keys]
idx = ret.index(value)
return ret, idx
else:
ret = [None] * size
for v, k in result:
ii = int(k.key.decode().split('/')[-1])
if ii < 0:
self.ctx.logger.error(f"rank {ii} error in sync")
ret[ii] = v.decode()
return ret, rank
else:
time.sleep(0.5)
def register_heartbeat(self, job_id, pod_id, ttl=10):
if hasattr(self, 'heartbeat_prefix'):
self.ctx.logger.warning("Heartbeat already done")
return
self.job_prefix = f'/paddle/{job_id}'
self.heartbeat_prefix = f'{self.job_prefix}/heartbeat'
self.client.delete_prefix(self.job_prefix)
lease = self.client.lease(ttl)
# self.client.delete_prefix(self.job_prefix)
beat_path = f"{self.heartbeat_prefix}/{pod_id}"
self.client.put(beat_path, pod_id.encode('latin-1'), lease=lease)
def _beat_watch(event):
self.ctx.status.restart()
beat_watch = self.client.add_watch_prefix_callback(
self.heartbeat_prefix, _beat_watch
)
def _heartbeat():
while not self.ctx.status.is_done():
try:
lease.refresh()
if pod_id not in self.fetch_peer_alive():
self.client.put(
beat_path, pod_id.encode('latin-1'), lease=lease
)
self.ctx.logger.debug("Heartbeat register again")
except Exception as e:
self.ctx.logger.error(f"Heartbeat error {e}")
time.sleep(ttl / 2)
self.ctx.logger.debug("Heartbeat done")
self.client.cancel_watch(beat_watch)
self.beat_thread = threading.Thread(
name='heartbeat', target=_heartbeat, daemon=True
)
self.beat_thread.start()
def fetch_peer_alive(self):
peer_alive = [
i[0].decode() for i in self.client.get_prefix(self.heartbeat_prefix)
]
self.ctx.logger.debug(f"peer alive {peer_alive}")
return peer_alive
def wait_peer_ready(self, replicas_min, replicas_max, timeout):
timeout = timeout if timeout > 1 else 3
end = time.time() + timeout
np_pre = len(self.fetch_peer_alive())
while not self.ctx.status.is_done() and time.time() < end:
np = len(self.fetch_peer_alive())
if np == replicas_max:
# maximum replicas reached, return immediately
return (True, replicas_max)
elif np != np_pre:
# replicas are changing, reset timeout
end = time.time() + timeout
np_pre = np
time.sleep(0.2)
else:
time.sleep(0.5)
np = len(self.fetch_peer_alive())
if np >= replicas_min and np <= replicas_max:
return (True, np)
else:
return (False, np)
def restart_peer(self):
self.client.delete_prefix(self.heartbeat_prefix)
def set_status(self, status):
assert self.client.put(
self.job_prefix,
status.encode('latin-1'),
lease=self.client.lease(600),
), f"set status failed {status}"
def get_status(self):
value = self.client.get(self.job_prefix)[0]
return value.decode() if value is not None else ''
def stop(self):
if hasattr(self, 'beat_thread'):
self.ctx.status.done()
# daemon thread
# self.beat_thread.join()
@@ -0,0 +1,239 @@
# 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 json
import os
import shutil
from .controller import Controller, ControllerMode
class PSController(Controller):
@classmethod
def enable(cls, ctx):
if (
ctx.args.run_mode == ControllerMode.PS
or ctx.args.server_num
or len(ctx.args.servers) > 0
or ctx.args.trainer_num
or len(ctx.args.trainers) > 0
):
ctx.logger.debug(f"{cls.__name__} enabled")
ctx.args.run_mode = ControllerMode.PS
return True
else:
return False
def build_pod(self):
if self.ctx.args.servers and self.ctx.args.trainers:
self._build_pod_with_args()
else:
self._build_pod_with_master()
def _build_pod_with_args(self):
if '127.0.0.1' in self.ctx.args.servers:
host = '127.0.0.1'
else:
host = self.ctx.node.ip
server_endpoints = list(self.ctx.args.servers.split(","))
trainer_endpoints = list(self.ctx.args.trainers.split(","))
servers = [
s for s in self.ctx.args.servers.split(",") if s.startswith(host)
]
trainers = [
s for s in self.ctx.args.trainers.split(",") if s.startswith(host)
]
server_num = len(servers)
trainer_num = len(trainers)
self.pod.replicas = server_num + trainer_num
self.save_pod_log([server_endpoints, trainer_endpoints])
import tempfile
gloo_rendezvous_dir = tempfile.mkdtemp()
if os.path.exists(gloo_rendezvous_dir):
shutil.rmtree(gloo_rendezvous_dir)
gloo_port = self.ctx.args.gloo_port
gloo_http = "{}:{}".format(server_endpoints[0].split(":")[0], gloo_port)
_gloo_envs = {
"PADDLE_GLOO_RENDEZVOUS": "3",
"PADDLE_GLOO_FS_PATH": gloo_rendezvous_dir,
"PADDLE_GLOO_HTTP_ENDPOINT": gloo_http,
"PADDLE_WITH_GLOO": self.ctx.args.with_gloo,
}
for i in range(server_num):
e = {
"PADDLE_PSERVERS_IP_PORT_LIST": self.ctx.args.servers,
"PADDLE_TRAINER_ENDPOINTS": self.ctx.args.trainers,
"PADDLE_PORT": servers[i].split(":")[1],
"PADDLE_ROLE": "PSERVER",
"TRAINING_ROLE": "PSERVER",
"PADDLE_TRAINERS_NUM": f"{len(trainer_endpoints)}",
"POD_IP": self.ctx.node.ip,
}
e.update(_gloo_envs)
log_file = f"serverlog.{i}"
self.add_container(envs=e, log_file=log_file)
trainer_rank_offset = 0
for s in trainer_endpoints:
if s.startswith(host):
break
else:
trainer_rank_offset += 1
for i in range(trainer_num):
e = {
"PADDLE_PSERVERS_IP_PORT_LIST": ",".join(server_endpoints),
"PADDLE_TRAINER_ENDPOINTS": ",".join(trainer_endpoints),
"PADDLE_PORT": trainers[i].split(":")[1],
"PADDLE_ROLE": "TRAINER",
"TRAINING_ROLE": "TRAINER",
"PADDLE_TRAINER_ID": f"{i + trainer_rank_offset}",
"PADDLE_TRAINERS_NUM": f"{len(trainer_endpoints)}",
"POD_IP": self.ctx.node.ip,
}
e.update(_gloo_envs)
log_file = f"workerlog.{i}"
self.add_container(envs=e, log_file=log_file)
def _build_pod_with_master(self):
self.pod.rank = int(self.ctx.args.rank)
server_num = self.ctx.args.server_num or 1
servers = [
f"{self.ctx.node.ip}:{p}"
for p in self.ctx.node.get_free_ports(server_num)
]
trainer_num = self.ctx.args.trainer_num or 1
trainers = [
f"{self.ctx.node.ip}:{p}"
for p in self.ctx.node.get_free_ports(trainer_num)
]
data = json.dumps(
{
'name': self.pod.name,
'rank': self.pod.rank,
'servers': servers,
'trainers': trainers,
'dtype': self.ctx.node.device.dtype,
'gloo_port': self.ctx.node.get_free_port(),
}
)
peer_list, rank = self.master.sync_peers(
f'/{self.job.id}/info',
self.pod.name,
data,
self.job.replicas,
self.pod.rank,
)
self.ctx.logger.debug(f"sync peers done {peer_list}")
peer_list = [json.loads(i) for i in peer_list]
self.save_pod_log(peer_list)
server_endpoints = [j for i in peer_list for j in i['servers']]
trainer_endpoints = [j for i in peer_list for j in i['trainers']]
# rank_offset = sum([i['replicas'] for i in peer_list[:rank]])
server_rank_offset = sum([len(i['servers']) for i in peer_list[:rank]])
trainer_rank_offset = sum(
[len(i['trainers']) for i in peer_list[:rank]]
)
self.pod.rank = rank
self.pod.replicas = server_num + trainer_num
import tempfile
gloo_rendezvous_dir = tempfile.mkdtemp()
if os.path.exists(gloo_rendezvous_dir):
shutil.rmtree(gloo_rendezvous_dir)
gloo_port = peer_list[0]['gloo_port']
gloo_http = "{}:{}".format(server_endpoints[0].split(":")[0], gloo_port)
_gloo_envs = {
"PADDLE_GLOO_RENDEZVOUS": "3",
"PADDLE_GLOO_FS_PATH": gloo_rendezvous_dir,
"PADDLE_GLOO_HTTP_ENDPOINT": gloo_http,
"PADDLE_WITH_GLOO": self.ctx.args.with_gloo,
}
for i in range(server_num):
e = {
"PADDLE_NNODES": f"{self.job.replicas}",
"PADDLE_PSERVERS_IP_PORT_LIST": ",".join(server_endpoints),
"PADDLE_TRAINER_ENDPOINTS": ",".join(trainer_endpoints),
"PADDLE_PORT": server_endpoints[i + server_rank_offset].split(
":"
)[1],
"PADDLE_ROLE": "PSERVER",
"TRAINING_ROLE": "PSERVER",
"PADDLE_TRAINERS_NUM": f"{len(trainer_endpoints)}",
"POD_IP": self.ctx.node.ip,
}
e.update(_gloo_envs)
log_file = f"serverlog.{i}"
self.add_container(envs=e, log_file=log_file)
for i in range(trainer_num):
e = {
"PADDLE_NNODES": f"{self.job.replicas}",
"PADDLE_PSERVERS_IP_PORT_LIST": ",".join(server_endpoints),
"PADDLE_TRAINER_ENDPOINTS": ",".join(trainer_endpoints),
"PADDLE_PORT": trainer_endpoints[i + trainer_rank_offset].split(
":"
)[1],
"PADDLE_ROLE": "TRAINER",
"TRAINING_ROLE": "TRAINER",
"PADDLE_TRAINER_ID": f"{i + trainer_rank_offset}",
"PADDLE_TRAINERS_NUM": f"{len(trainer_endpoints)}",
"POD_IP": self.ctx.node.ip,
}
e.update(_gloo_envs)
log_file = f"workerlog.{i}"
self.add_container(envs=e, log_file=log_file)
''' NEW VERSION
for i in range(server_num):
e = {
"PADDLE_PSERVER_ENDPOINTS": ",".join(server_endpoints),
"PADDLE_TRAINER_ENDPOINTS": ",".join(trainer_endpoints),
"PADDLE_ROLE": "PSERVER",
"PADDLE_RANK": "{}".format(i + server_rank_offset),
}
log_tag = "ps.{}".format(i)
self.add_container(envs=e, log_tag=log_tag)
for i in range(trainer_num):
e = {
"PADDLE_PSERVER_ENDPOINTS": ",".join(server_endpoints),
"PADDLE_TRAINER_ENDPOINTS": ",".join(trainer_endpoints),
"PADDLE_ROLE": "TRAINER_CPU",
"PADDLE_RANK": "{}".format(i + trainer_rank_offset),
}
log_tag = "trainer.{}".format(i)
self.add_container(envs=e, log_tag=log_tag)
'''
@@ -0,0 +1,91 @@
# 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 json
from .controller import Controller, ControllerMode
class RpcController(Controller):
@classmethod
def enable(cls, ctx):
if ctx.args.run_mode == ControllerMode.RPC:
ctx.logger.debug(f"{cls.__name__} enabled")
return True
else:
return False
def build_pod(self):
assert self.ctx.args.master is not None, (
"Master is None, Please set master address!"
)
self._build_pod_with_master()
def _build_pod_with_master(self):
# nproc_per_node
self.pod.replicas = self.pod_replicas()
# rank will be reset when restart
self.pod.rank = int(self.ctx.args.rank)
port = self.ctx.node.get_free_port()
# compatible
endpoints = [
f"{self.ctx.node.ip}:{p}"
for p in self.ctx.node.get_free_ports(self.pod.replicas)
]
data = json.dumps(
{
"name": self.pod.name,
"rank": self.pod.rank,
"replicas": self.pod.replicas,
"dtype": self.ctx.node.device.dtype,
"candidate": f"{self.ctx.node.ip}:{port}",
"endpoints": ",".join(endpoints),
}
)
peer_list, rank = self.master.sync_peers(
f"/{self.job.id}/info",
self.pod.name,
data,
self.job.replicas,
self.pod.rank,
)
self.pod.rank = rank
if len(peer_list) < 1:
return False
peer_list = [json.loads(i) for i in peer_list]
self.ctx.logger.debug(f"sync peers done {peer_list}")
self.save_pod_log(peer_list)
global_size = sum([i["replicas"] for i in peer_list])
rank_offset = sum([i["replicas"] for i in peer_list[:rank]])
rpc_master = peer_list[0]["candidate"]
self.pod.reset()
for i in range(self.pod.replicas):
e = {
"PADDLE_MASTER_ENDPOINT": rpc_master,
"PADDLE_WORKER_ENDPOINT": endpoints[i],
"PADDLE_TRAINER_ID": f"{i + rank_offset}",
"PADDLE_TRAINERS_NUM": f"{global_size}",
}
log_file = f"workerlog.{i + rank_offset}"
self.add_container(envs=e, log_file=log_file)
return True
@@ -0,0 +1,106 @@
# 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 os
import time
from threading import Thread
import paddle
from ..utils.nvsmi import get_gpu_info, get_gpu_process, get_gpu_util
class Watcher:
def __init__(self, ctx):
self.ctx = ctx
self.interval = 5
self.gpu_util = []
if not self.ctx.args.enable_gpu_log:
return
if paddle.is_compiled_with_rocm():
return
# gpu log file
self.gpus = self.ctx.args.devices or self.ctx.node.device.labels
if len(self.gpus) > 0:
fn = os.path.join(
self.ctx.args.log_dir, f"{self.ctx.args.job_id}.gpu.log"
)
os.makedirs(os.path.dirname(fn), exist_ok=True)
self.gpu_fd = open(fn, 'w')
else:
return
# start
self.proc = Thread(target=self.watch)
self.proc.daemon = True
self.proc.start()
def watch(self):
if not len(self.gpus) > 0:
return
self._print_gpu_info()
util_key = "index,utilization_gpu,memory_total,memory_used,memory_free,timestamp"
self.gpu_fd.write(util_key)
self.gpu_fd.write('\n')
while not self.ctx.status.is_done():
self._save_gpu_log(util_key)
time.sleep(self.interval)
if hasattr(self, "gpu_fd"):
self.gpu_fd.close()
def _print_gpu_info(self):
try:
info_key = "index,uuid,driver_version,name,gpu_serial,display_active,display_mode"
self.gpu_fd.write(info_key)
self.gpu_fd.write('\n')
for line in get_gpu_info(self.gpus):
self.gpu_fd.write(line.str(info_key))
self.gpu_fd.write('\n')
self.gpu_fd.write('\n')
process_key = "pid,process_name,gpu_uuid,gpu_name,used_memory"
self.gpu_fd.write(process_key)
self.gpu_fd.write('\n')
for line in get_gpu_process(self.gpus):
self.gpu_fd.write(line.str(process_key))
self.gpu_fd.write('\n')
self.gpu_fd.write('\n')
self.gpu_fd.flush()
except Exception as e:
self.ctx.logger.warning(f"save gpu info failed: {e!s}")
def _save_gpu_log(self, util_key):
try:
for line in get_gpu_util(self.gpus):
self.gpu_fd.write(line.str(util_key))
self.gpu_fd.write('\n')
self.gpu_fd.flush()
except Exception as e:
self.ctx.logger.warning(f"save gpu log failed: {e!s}")
def stop(self):
if hasattr(self, "proc"):
# daemon without join
# self.proc.join()
pass