chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,589 @@
|
||||
# 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 platform
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from contextlib import closing
|
||||
|
||||
from paddle.distributed.fleet.launch_utils import get_backend_by_compile_flag
|
||||
from paddle.utils import strtobool
|
||||
|
||||
from ..utils.log_utils import get_logger
|
||||
|
||||
logger = get_logger("INFO", "root")
|
||||
|
||||
|
||||
def get_cluster_from_args(args, selected_gpus):
|
||||
node_ips = [x.strip() for x in args.cluster_node_ips.split(',')]
|
||||
node_ip = args.node_ip
|
||||
node_rank = node_ips.index(node_ip)
|
||||
|
||||
logger.debug(
|
||||
f"parsed from args:node_ips:{node_ips} node_ip:{node_ip} node_rank:{node_rank}"
|
||||
)
|
||||
|
||||
free_ports = None
|
||||
if (
|
||||
not args.use_paddlecloud
|
||||
and len(node_ips) <= 1
|
||||
and args.started_port is None
|
||||
):
|
||||
free_ports = find_free_ports(len(selected_gpus))
|
||||
if free_ports is not None:
|
||||
free_ports = list(free_ports)
|
||||
else:
|
||||
started_port = 6070
|
||||
if args.started_port is not None:
|
||||
started_port = args.started_port
|
||||
|
||||
free_ports = list(
|
||||
range(started_port, started_port + len(selected_gpus))
|
||||
)
|
||||
|
||||
trainer_endpoints = []
|
||||
for ip in node_ips:
|
||||
trainer_endpoints.append([f"{ip}:{port}" for port in free_ports])
|
||||
return get_cluster(node_ips, node_ip, trainer_endpoints, selected_gpus)
|
||||
|
||||
|
||||
def get_gpus(selected_gpus):
|
||||
if selected_gpus is None:
|
||||
from paddle.framework import core
|
||||
|
||||
gpus_num = core.get_cuda_device_count()
|
||||
gpus = [str(x) for x in range(0, gpus_num)]
|
||||
else:
|
||||
cuda_visible_devices = os.getenv("CUDA_VISIBLE_DEVICES")
|
||||
if cuda_visible_devices is None or cuda_visible_devices == "":
|
||||
gpus = [x.strip() for x in selected_gpus.split(',')]
|
||||
else:
|
||||
# change selected_gpus into relative values
|
||||
# e.g. CUDA_VISIBLE_DEVICES=4,5,6,7; args.selected_gpus=4,5,6,7;
|
||||
# therefore selected_gpus=0,1,2,3
|
||||
cuda_visible_devices_list = cuda_visible_devices.split(',')
|
||||
for x in selected_gpus.split(','):
|
||||
assert x in cuda_visible_devices_list, (
|
||||
"Can't find "
|
||||
f"your selected_gpus {x} in CUDA_VISIBLE_DEVICES[{cuda_visible_devices}]."
|
||||
)
|
||||
gpus = [
|
||||
cuda_visible_devices_list.index(x.strip())
|
||||
for x in selected_gpus.split(',')
|
||||
]
|
||||
logger.info(
|
||||
f"Change selected_gpus into relative values. --ips:{selected_gpus} "
|
||||
f"will change into relative_ips:{gpus} according to your "
|
||||
f"CUDA_VISIBLE_DEVICES:{cuda_visible_devices_list}"
|
||||
)
|
||||
|
||||
return gpus
|
||||
|
||||
|
||||
class Hdfs:
|
||||
def __init__(self):
|
||||
self.hdfs_ugi = None
|
||||
self.hdfs_name = None
|
||||
self.hdfs_path = None
|
||||
|
||||
def is_valid(self):
|
||||
return (
|
||||
self.hdfs_ugi is not None
|
||||
and self.hdfs_name is not None
|
||||
and self.hdfs_path is not None
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"hdfs_ugi:{self.hdfs_ugi} hdfs_name:{self.hdfs_name} hdfs_path{self.hdfs_path}"
|
||||
|
||||
def __eq__(self, n):
|
||||
return (
|
||||
self.hdfs_ugi == n.hdfs_ugi
|
||||
and self.hdfs_name == n.hdfs_name
|
||||
and self.hdfs_path == n.hdfs_path
|
||||
)
|
||||
|
||||
def __ne__(self, n):
|
||||
return not self == n
|
||||
|
||||
|
||||
class Cluster:
|
||||
def __init__(self, hdfs):
|
||||
self.job_server = None
|
||||
self.pods = []
|
||||
self.hdfs = None
|
||||
self.job_stage_flag = None
|
||||
|
||||
def __str__(self):
|
||||
return f"job_server:{self.job_server} pods:{[str(pod) for pod in self.pods]} job_stage_flag:{self.job_stage_flag} hdfs:{self.hdfs}"
|
||||
|
||||
def __eq__(self, cluster):
|
||||
if len(self.pods) != len(cluster.pods):
|
||||
return False
|
||||
|
||||
for a, b in zip(self.pods, cluster.pods):
|
||||
if a != b:
|
||||
return False
|
||||
|
||||
if self.job_stage_flag != cluster.job_stage_flag:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ne__(self, cluster):
|
||||
return not self.__eq__(cluster)
|
||||
|
||||
def update_pods(self, cluster):
|
||||
self.pods = copy.copy(cluster.pods)
|
||||
|
||||
def trainers_nranks(self):
|
||||
return len(self.trainers_endpoints())
|
||||
|
||||
def pods_nranks(self):
|
||||
return len(self.pods)
|
||||
|
||||
def trainers_endpoints(self):
|
||||
r = []
|
||||
for pod in self.pods:
|
||||
for t in pod.trainers:
|
||||
r.append(t.endpoint)
|
||||
return r
|
||||
|
||||
def pods_endpoints(self):
|
||||
r = []
|
||||
for pod in self.pods:
|
||||
ep = f"{pod.addr}:{pod.port}"
|
||||
assert pod.port is not None and pod.addr is not None, (
|
||||
f"{ep} not a valid endpoint"
|
||||
)
|
||||
r.append(ep)
|
||||
|
||||
return r
|
||||
|
||||
def get_pod_by_id(self, pod_id):
|
||||
for pod in self.pods:
|
||||
if str(pod_id) == str(pod.id):
|
||||
return pod
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class JobServer:
|
||||
def __init__(self):
|
||||
self.endpoint = None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.endpoint}"
|
||||
|
||||
def __eq__(self, j):
|
||||
return self.endpoint == j.endpoint
|
||||
|
||||
def __ne__(self, j):
|
||||
return not self == j
|
||||
|
||||
|
||||
class Trainer:
|
||||
def __init__(self):
|
||||
self.gpus = []
|
||||
self.endpoint = None
|
||||
self.rank = None
|
||||
|
||||
def __str__(self):
|
||||
return f"gpu:{self.gpus} endpoint:{self.endpoint} rank:{self.rank}"
|
||||
|
||||
def __eq__(self, t):
|
||||
if len(self.gpus) != len(t.gpus):
|
||||
return False
|
||||
|
||||
if self.endpoint != t.endpoint or self.rank != t.rank:
|
||||
return False
|
||||
|
||||
for a, b in zip(self.gpus, t.gpus):
|
||||
if a != b:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ne__(self, t):
|
||||
return not self == t
|
||||
|
||||
def get_rank(self):
|
||||
return self.rank
|
||||
|
||||
|
||||
class Pod:
|
||||
def __init__(self):
|
||||
self.rank = None
|
||||
self.id = None
|
||||
self.addr = None
|
||||
self.port = None
|
||||
self.trainers = []
|
||||
self.gpus = []
|
||||
|
||||
def __str__(self):
|
||||
return f"rank:{self.rank} id:{self.id} addr:{self.addr} port:{self.port} visible_gpu:{self.gpus} trainers:{[str(t) for t in self.trainers]}"
|
||||
|
||||
def __eq__(self, pod):
|
||||
if (
|
||||
self.rank != pod.rank
|
||||
or self.id != pod.id
|
||||
or self.addr != pod.addr
|
||||
or self.port != pod.port
|
||||
):
|
||||
logger.debug(f"pod {self} != {pod}")
|
||||
return False
|
||||
|
||||
if len(self.trainers) != len(pod.trainers):
|
||||
logger.debug(f"trainers {self.trainers} != {pod.trainers}")
|
||||
return False
|
||||
|
||||
for i in range(len(self.trainers)):
|
||||
if self.trainers[i] != pod.trainers[i]:
|
||||
logger.debug(f"trainer {self.trainers[i]} != {pod.trainers[i]}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ne__(self, pod):
|
||||
return not self == pod
|
||||
|
||||
def parse_response(self, res_pods):
|
||||
pass
|
||||
|
||||
def get_visible_gpus(self):
|
||||
r = ""
|
||||
for g in self.gpus:
|
||||
r += f"{g},"
|
||||
|
||||
assert r != "", f"this pod {self} can't see any gpus"
|
||||
|
||||
r = r[:-1]
|
||||
return r
|
||||
|
||||
|
||||
def get_cluster(node_ips, node_ip, trainer_endpoints, selected_gpus):
|
||||
assert type(trainer_endpoints) is list, "trainer_endpoints must be list"
|
||||
cluster = Cluster(hdfs=None)
|
||||
trainer_rank = 0
|
||||
for node_rank, ip in enumerate(node_ips):
|
||||
pod = Pod()
|
||||
pod.rank = node_rank
|
||||
pod.addr = ip
|
||||
cur_node_endpoints = trainer_endpoints[node_rank]
|
||||
# when use paddlecloud, endpoints may > selected_gpus(user_defined)
|
||||
assert len(cur_node_endpoints) >= len(selected_gpus), (
|
||||
"current trainer_endpoints size should be greater equal than selected_gpus size."
|
||||
)
|
||||
for i in range(len(selected_gpus)):
|
||||
trainer = Trainer()
|
||||
trainer.gpus.append(selected_gpus[i])
|
||||
trainer.endpoint = f"{cur_node_endpoints[i]}"
|
||||
trainer.rank = trainer_rank
|
||||
trainer_rank += 1
|
||||
|
||||
pod.trainers.append(trainer)
|
||||
cluster.pods.append(pod)
|
||||
|
||||
pod_rank = node_ips.index(node_ip)
|
||||
return cluster, cluster.pods[pod_rank]
|
||||
|
||||
|
||||
def terminate_local_procs(procs):
|
||||
for p in procs:
|
||||
if p.proc.poll() is None:
|
||||
p.proc.terminate()
|
||||
if p.log_fn:
|
||||
p.log_fn.close()
|
||||
logger.debug(f"terminate process id:{p.proc.pid}")
|
||||
|
||||
# wait all process terminated
|
||||
time.sleep(3)
|
||||
for step in range(0, 50):
|
||||
alive = False
|
||||
for p in procs:
|
||||
if p.proc.poll() is None: # not terminate
|
||||
os.kill(p.proc.pid, signal.SIGKILL)
|
||||
alive = True
|
||||
|
||||
if not alive:
|
||||
logger.info("terminate all the procs")
|
||||
return
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
logger.fatal("can't kill all process and exit")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_host_name_ip():
|
||||
try:
|
||||
host_name = socket.gethostname()
|
||||
host_ip = socket.gethostbyname(host_name)
|
||||
return host_name, host_ip
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def add_arguments(argname, type, default, help, argparser, **kwargs):
|
||||
"""Add argparse's argument.
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import argparse
|
||||
>>> from paddle.distributed.utils import launch_utils
|
||||
>>> parser = argparse.ArgumentParser()
|
||||
>>> launch_utils.add_arguments("name", str, "Jonh", "User name.", parser)
|
||||
>>> args = parser.parse_args()
|
||||
|
||||
"""
|
||||
type = strtobool if type == bool else type
|
||||
argparser.add_argument(
|
||||
"--" + argname,
|
||||
default=default,
|
||||
type=type,
|
||||
help=help + ' Default: %(default)s.',
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def find_free_ports(num):
|
||||
def __free_port():
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(('', 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
port_set = set()
|
||||
step = 0
|
||||
while True:
|
||||
port = __free_port()
|
||||
if port not in port_set:
|
||||
port_set.add(port)
|
||||
|
||||
if len(port_set) >= num:
|
||||
return port_set
|
||||
|
||||
step += 1
|
||||
if step > 100:
|
||||
print(
|
||||
"can't find available port and use the specified static port now!"
|
||||
)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _prepare_trainer_env(cluster, trainer, backend=None):
|
||||
if backend is None:
|
||||
backend = get_backend_by_compile_flag() # for compatibility
|
||||
if backend == 'bkcl':
|
||||
proc_env = {
|
||||
"FLAGS_selected_xpus": "{}".format(
|
||||
",".join([str(g) for g in trainer.gpus])
|
||||
),
|
||||
"PADDLE_TRAINER_ID": str(trainer.rank),
|
||||
"PADDLE_CURRENT_ENDPOINT": str(trainer.endpoint),
|
||||
"PADDLE_TRAINERS_NUM": str(cluster.trainers_nranks()),
|
||||
"PADDLE_TRAINER_ENDPOINTS": ",".join(cluster.trainers_endpoints()),
|
||||
}
|
||||
elif backend == 'nccl':
|
||||
proc_env = {
|
||||
"FLAGS_selected_gpus": "{}".format(
|
||||
",".join([str(g) for g in trainer.gpus])
|
||||
),
|
||||
"PADDLE_TRAINER_ID": str(trainer.rank),
|
||||
"PADDLE_CURRENT_ENDPOINT": str(trainer.endpoint),
|
||||
"PADDLE_TRAINERS_NUM": str(cluster.trainers_nranks()),
|
||||
"PADDLE_TRAINER_ENDPOINTS": ",".join(cluster.trainers_endpoints()),
|
||||
}
|
||||
elif backend == 'gloo':
|
||||
# NOTE (xiongkun) default fall back into cpu only
|
||||
proc_env = {
|
||||
"PADDLE_TRAINER_ID": str(trainer.rank),
|
||||
"PADDLE_CURRENT_ENDPOINT": str(trainer.endpoint),
|
||||
"PADDLE_TRAINERS_NUM": str(cluster.trainers_nranks()),
|
||||
"PADDLE_TRAINER_ENDPOINTS": ",".join(cluster.trainers_endpoints()),
|
||||
"PADDLE_DISTRI_BACKEND": backend, # only add here, other will be auto
|
||||
}
|
||||
elif backend == 'xccl':
|
||||
from paddle.framework import core
|
||||
|
||||
custom_device_name = core.get_all_custom_device_type()[0]
|
||||
proc_env = {
|
||||
f"FLAGS_selected_{custom_device_name}s": "{}".format(
|
||||
",".join([str(g) for g in trainer.gpus])
|
||||
),
|
||||
"PADDLE_TRAINER_ID": str(trainer.rank),
|
||||
"PADDLE_CURRENT_ENDPOINT": str(trainer.endpoint),
|
||||
"PADDLE_TRAINERS_NUM": str(cluster.trainers_nranks()),
|
||||
"PADDLE_TRAINER_ENDPOINTS": ",".join(cluster.trainers_endpoints()),
|
||||
}
|
||||
else:
|
||||
raise ValueError("backend must be one of 'gloo, nccl, bkcl'")
|
||||
|
||||
return proc_env
|
||||
|
||||
|
||||
class TrainerProc:
|
||||
def __init__(self):
|
||||
self.proc = None
|
||||
self.log_fn = None
|
||||
self.log_offset = None
|
||||
self.rank = None
|
||||
self.local_rank = None
|
||||
self.cmd = None
|
||||
|
||||
|
||||
def start_local_trainers(
|
||||
cluster, pod, training_script, training_script_args, log_dir=None
|
||||
):
|
||||
current_env = copy.copy(os.environ.copy())
|
||||
# paddle broadcast ncclUniqueId use socket, and
|
||||
# proxy maybe make trainers unreachable, so delete them.
|
||||
# if we set them to "", grpc will log error message "bad uri"
|
||||
# so just delete them.
|
||||
current_env.pop("http_proxy", None)
|
||||
current_env.pop("https_proxy", None)
|
||||
|
||||
procs = []
|
||||
for idx, t in enumerate(pod.trainers):
|
||||
proc_env = _prepare_trainer_env(cluster, t)
|
||||
current_env.update(proc_env)
|
||||
|
||||
logger.debug(f"trainer proc env:{current_env}")
|
||||
|
||||
cmd = [sys.executable, "-u", training_script, *training_script_args]
|
||||
|
||||
logger.info(f"start trainer proc:{cmd} env:{proc_env}")
|
||||
|
||||
fn = None
|
||||
if log_dir is not None:
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
fn = open(f"{log_dir}/workerlog.{idx}", "a")
|
||||
proc = subprocess.Popen(cmd, env=current_env, stdout=fn, stderr=fn)
|
||||
else:
|
||||
proc = subprocess.Popen(cmd, env=current_env)
|
||||
|
||||
tp = TrainerProc()
|
||||
tp.proc = proc
|
||||
tp.rank = t.rank
|
||||
tp.local_rank = idx
|
||||
tp.log_fn = fn
|
||||
tp.log_offset = fn.tell() if fn else None
|
||||
tp.cmd = cmd
|
||||
|
||||
procs.append(tp)
|
||||
|
||||
return procs
|
||||
|
||||
|
||||
def pull_worker_log(tp):
|
||||
if tp.log_fn:
|
||||
with open(tp.log_fn.name, 'r') as fin:
|
||||
fin.seek(tp.log_offset, 0)
|
||||
for line in fin:
|
||||
try:
|
||||
sys.stdout.write(line)
|
||||
except UnicodeEncodeError:
|
||||
sys.stdout.write(
|
||||
'UnicodeEncodeError occurs at this line. '
|
||||
f'Please refer to the original log file "{tp.log_fn.name}"\n'
|
||||
)
|
||||
tp.log_offset = fin.tell()
|
||||
|
||||
|
||||
def watch_local_trainers(procs, nranks):
|
||||
try:
|
||||
error = False
|
||||
error_rank = []
|
||||
# wait all process finish or one error
|
||||
alive = False
|
||||
for p in procs:
|
||||
if p.log_fn and p.local_rank == 0:
|
||||
pull_worker_log(p)
|
||||
|
||||
ret = p.proc.poll()
|
||||
if ret is None:
|
||||
alive = True
|
||||
elif ret != 0:
|
||||
error = True
|
||||
error_rank.append(p.rank)
|
||||
|
||||
if error:
|
||||
terminate_local_procs(procs)
|
||||
sys.exit(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.warning("KeyboardInterrupt, exit")
|
||||
terminate_local_procs(procs)
|
||||
raise
|
||||
except SystemExit:
|
||||
logger.error(
|
||||
f"ABORT!!! Out of all {nranks} trainers, the trainer process with rank={error_rank} was aborted. Please check its log."
|
||||
)
|
||||
terminate_local_procs(procs)
|
||||
raise
|
||||
except:
|
||||
logger.error(
|
||||
f"ABORT!!! Out of all {nranks} trainers, the trainer process with rank={error_rank} was aborted. Please check its log."
|
||||
)
|
||||
terminate_local_procs(procs)
|
||||
raise
|
||||
|
||||
return alive
|
||||
|
||||
|
||||
def _print_arguments(args):
|
||||
print("----------- Configuration Arguments -----------")
|
||||
for arg, value in sorted(vars(args).items()):
|
||||
print(f"{arg}: {value}")
|
||||
print("------------------------------------------------")
|
||||
|
||||
|
||||
def filter_pids(processes: Sequence[str], self_pid: int) -> list[int]:
|
||||
"""Filter valid PIDs from a list of strings, excluding the current self_pid."""
|
||||
pids_to_kill = []
|
||||
for process in processes:
|
||||
pid_str = process.strip()
|
||||
if not pid_str.isdigit():
|
||||
continue
|
||||
pid_int = int(pid_str)
|
||||
if pid_int == self_pid:
|
||||
continue
|
||||
pids_to_kill.append(pid_int)
|
||||
return pids_to_kill
|
||||
|
||||
|
||||
def terminate_processes(processes: Sequence[int]) -> bool:
|
||||
"""
|
||||
Terminate a list of processes by their PIDs.
|
||||
Returns True if all processes were successfully terminated (or already dead).
|
||||
Returns False if any process failed to terminate due to permissions.
|
||||
"""
|
||||
sig = signal.SIGKILL if platform.system() != "Windows" else signal.SIGTERM
|
||||
success = True
|
||||
for pid in processes:
|
||||
try:
|
||||
os.kill(pid, sig)
|
||||
except ProcessLookupError:
|
||||
# Target already exited.
|
||||
pass
|
||||
except PermissionError:
|
||||
success = False
|
||||
return success
|
||||
Reference in New Issue
Block a user