Files
paddlepaddle--paddle/python/paddle/distributed/utils/launch_utils.py
T
2026-07-13 12:40:42 +08:00

590 lines
17 KiB
Python

# 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