chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# 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__ = []
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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.
|
||||
|
||||
from .main import launch
|
||||
|
||||
launch()
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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 logging
|
||||
|
||||
from paddle.distributed.launch import plugins
|
||||
|
||||
from .args_envs import env_args_mapping, fetch_envs, parse_args
|
||||
from .node import Node
|
||||
from .status import Status
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, enable_plugin=True):
|
||||
self.args, self.unknown_args = parse_args()
|
||||
self.envs = fetch_envs()
|
||||
|
||||
self.set_env_in_args()
|
||||
|
||||
self.node = Node()
|
||||
self.status = Status()
|
||||
|
||||
self.logger = self.get_logger()
|
||||
|
||||
# design for event queue, later
|
||||
self.events = []
|
||||
|
||||
if enable_plugin:
|
||||
self._enable_plugin()
|
||||
self.max_time_per_task = -1
|
||||
self.run_best = False
|
||||
|
||||
def print(self):
|
||||
self.logger.info("----------- Configuration ----------------------")
|
||||
for arg, value in sorted(vars(self.args).items()):
|
||||
self.logger.info(f"{arg}: {value}")
|
||||
self.logger.info("--------------------------------------------------")
|
||||
|
||||
def is_legacy_mode(self):
|
||||
if self.args.legacy:
|
||||
return True
|
||||
|
||||
if self.args.master:
|
||||
return False
|
||||
|
||||
if len(self.unknown_args) > 0:
|
||||
self.logger.warning(
|
||||
f"Compatible mode enable with args {self.unknown_args}"
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_auto_tuner_mode(self):
|
||||
if self.args.auto_tuner_json:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_envs(self):
|
||||
return self.envs.copy()
|
||||
|
||||
def set_envs(self, env={}):
|
||||
env = {k: v for k, v in env.items() if isinstance(v, str)}
|
||||
self.envs.update(env)
|
||||
|
||||
def _enable_plugin(self):
|
||||
for pl in plugins.enabled_plugins:
|
||||
pl(self)
|
||||
|
||||
def get_logger(self, level=logging.INFO):
|
||||
logger = logging.getLogger("LAUNCH")
|
||||
# forbid the child logger pass on to its parent
|
||||
logger.propagate = False
|
||||
logger.setLevel(self.args.log_level.upper() or level)
|
||||
formatter = logging.Formatter(
|
||||
fmt='%(name)s %(levelname)s %(asctime)s %(message)s'
|
||||
)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
return logger
|
||||
|
||||
def continuous_log(self) -> bool:
|
||||
if self.args.log_level.upper() in ['DEBUG', 'ERROR']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_env_in_args(self):
|
||||
for k, v in env_args_mapping.items():
|
||||
attr, attr_type = v
|
||||
if k in self.envs:
|
||||
print(
|
||||
f"LAUNCH WARNING args {attr} will be overridden by env: {k} value: {self.envs[k]}"
|
||||
)
|
||||
setattr(self.args, attr, attr_type(self.envs[k]))
|
||||
@@ -0,0 +1,246 @@
|
||||
# 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 warnings
|
||||
from argparse import REMAINDER, ArgumentParser
|
||||
|
||||
from paddle.utils import strtobool
|
||||
|
||||
env_args_mapping = {
|
||||
'POD_IP': ('host', str),
|
||||
'PADDLE_MASTER': ('master', str),
|
||||
'PADDLE_DEVICES': ('devices', str),
|
||||
'PADDLE_NNODES': ('nnodes', str),
|
||||
'PADDLE_RUN_MODE': ('run_mode', str),
|
||||
'PADDLE_LOG_LEVEL': ('log_level', str),
|
||||
'PADDLE_LOG_OVERWRITE': ('log_overwrite', strtobool),
|
||||
'PADDLE_SORT_IP': ('sort_ip', strtobool),
|
||||
'PADDLE_NPROC_PER_NODE': ('nproc_per_node', int),
|
||||
'PADDLE_JOB_ID': ('job_id', str),
|
||||
'PADDLE_RANK': ('rank', int),
|
||||
'PADDLE_LOG_DIR': ('log_dir', str),
|
||||
'PADDLE_MAX_RESTART': ('max_restart', int),
|
||||
'PADDLE_ELASTIC_LEVEL': ('elastic_level', int),
|
||||
'PADDLE_ELASTIC_TIMEOUT': ('elastic_timeout', int),
|
||||
'PADDLE_SERVER_NUM': ('server_num', int),
|
||||
'PADDLE_TRAINER_NUM': ('trainer_num', int),
|
||||
'PADDLE_SERVERS_ENDPOINTS': ('servers', str),
|
||||
'PADDLE_TRAINERS_ENDPOINTS': ('trainers', str),
|
||||
'PADDLE_GLOO_PORT': ('gloo_port', int),
|
||||
'PADDLE_WITH_GLOO': ('with_gloo', str),
|
||||
'PADDLE_START_PORT': ('start_port', int),
|
||||
'PADDLE_IPS': ('ips', str),
|
||||
"PADDLE_AUTO_PARALLEL_CONFIG": ('auto_parallel_config', str),
|
||||
'PADDLE_AUTO_CLUSTER': ('auto_cluster_config', strtobool),
|
||||
}
|
||||
|
||||
|
||||
def fetch_envs():
|
||||
for proxy_key in ("http_proxy", "https_proxy"):
|
||||
if os.environ.get(proxy_key) is not None:
|
||||
os.environ[f"{proxy_key}_original"] = os.environ.pop(proxy_key)
|
||||
warnings.warn(
|
||||
f"Unset '{proxy_key}' to ensure stable NCCL communication in distributed training "
|
||||
f"(backed up as '{proxy_key}_original').",
|
||||
category=UserWarning,
|
||||
)
|
||||
|
||||
return os.environ.copy()
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser()
|
||||
|
||||
base_group = parser.add_argument_group("Base Parameters")
|
||||
|
||||
base_group.add_argument(
|
||||
"--master",
|
||||
type=str,
|
||||
default=None,
|
||||
help="the master/rendezvous server, ip:port",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--legacy", type=strtobool, default=False, help="use legacy launch"
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--rank", type=int, default=-1, help="the node rank"
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--log_level", type=str, default="INFO", help="log level. Default INFO"
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--log_overwrite",
|
||||
type=strtobool,
|
||||
default=False,
|
||||
help="overwrite exits logfiles. Default False",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--sort_ip",
|
||||
type=strtobool,
|
||||
default=False,
|
||||
help="rank node by ip. Default False",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--enable_gpu_log",
|
||||
type=strtobool,
|
||||
default=True,
|
||||
help="enable capture gpu log while running. Default True",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--nnodes",
|
||||
type=str,
|
||||
default="1",
|
||||
help="the number of nodes, i.e. pod/node number",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--nproc_per_node",
|
||||
type=int,
|
||||
default=None,
|
||||
help="the number of processes in a pod",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--log_dir",
|
||||
type=str,
|
||||
default="log",
|
||||
help="the path for each process's log. Default ./log",
|
||||
)
|
||||
base_group.add_argument(
|
||||
"--run_mode",
|
||||
type=str,
|
||||
default=None,
|
||||
help="run mode of the job, collective/ps/ps-heter",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--job_id",
|
||||
type=str,
|
||||
default="default",
|
||||
help="unique id of the job. Default default",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--devices",
|
||||
"--gpus",
|
||||
"--npus",
|
||||
"--xpus",
|
||||
type=str,
|
||||
default=None,
|
||||
help="accelerate devices. as --gpus,npus,xpus",
|
||||
)
|
||||
|
||||
base_group.add_argument("--host", type=str, default=None, help="host ip")
|
||||
|
||||
base_group.add_argument(
|
||||
"--ips",
|
||||
type=str,
|
||||
default=None,
|
||||
help="nodes ips, e.g. 10.10.1.1,10.10.1.2",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--start_port", type=int, default=6070, help="fix port start with"
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--auto_parallel_config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="auto parallel config file absolute path, the file should be json format",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--auto_cluster_config",
|
||||
type=strtobool,
|
||||
default=0,
|
||||
help="auto parallel auto cluster config switch",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"training_script",
|
||||
type=str,
|
||||
help="the full path of py script,"
|
||||
"followed by arguments for the "
|
||||
"training script",
|
||||
)
|
||||
|
||||
base_group.add_argument(
|
||||
"--auto_tuner_json",
|
||||
type=str,
|
||||
default=None,
|
||||
help="auto tuner json file path",
|
||||
)
|
||||
|
||||
base_group.add_argument('training_script_args', nargs=REMAINDER)
|
||||
|
||||
ps_group = parser.add_argument_group("Parameter-Server Parameters")
|
||||
# for parameter server
|
||||
ps_group.add_argument(
|
||||
"--servers", type=str, default='', help="servers endpoints full list"
|
||||
)
|
||||
ps_group.add_argument(
|
||||
"--trainers", type=str, default='', help="trainers endpoints full list"
|
||||
)
|
||||
|
||||
ps_group.add_argument(
|
||||
"--trainer_num", type=int, default=None, help="number of trainers"
|
||||
)
|
||||
ps_group.add_argument(
|
||||
"--server_num", type=int, default=None, help="number of servers"
|
||||
)
|
||||
ps_group.add_argument(
|
||||
"--gloo_port", type=int, default=6767, help="gloo http port"
|
||||
)
|
||||
ps_group.add_argument(
|
||||
"--with_gloo", type=str, default="1", help="use gloo or not"
|
||||
)
|
||||
|
||||
# parameter elastic mode
|
||||
elastic_group = parser.add_argument_group("Elastic Parameters")
|
||||
elastic_group.add_argument(
|
||||
"--max_restart",
|
||||
type=int,
|
||||
default=3,
|
||||
help="the times can restart. Default 3",
|
||||
)
|
||||
|
||||
elastic_group.add_argument(
|
||||
"--elastic_level",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="elastic level: -1 disable, 0 failed exit, peers hold, 1 internal restart",
|
||||
)
|
||||
|
||||
elastic_group.add_argument(
|
||||
"--elastic_timeout",
|
||||
type=int,
|
||||
default=30,
|
||||
help="seconds to wait before elastic job begin to train",
|
||||
)
|
||||
|
||||
args = parser.parse_known_args()
|
||||
env_rank = int(os.getenv('PADDLE_TRAINER_ID', -1))
|
||||
if env_rank >= 0:
|
||||
assert hasattr(args[0], "rank")
|
||||
args[0].rank = env_rank
|
||||
return args
|
||||
@@ -0,0 +1,181 @@
|
||||
# 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
|
||||
|
||||
# (TODO: GhostScreaming) It will be removed later.
|
||||
from paddle.base import core
|
||||
from paddle.base.core import get_all_custom_device_type
|
||||
from paddle.device import get_available_custom_device
|
||||
|
||||
|
||||
class DeviceType:
|
||||
CPU = 'cpu'
|
||||
GPU = 'gpu'
|
||||
XPU = 'xpu'
|
||||
IPU = 'ipu'
|
||||
CUSTOM_DEVICE = 'custom_device'
|
||||
|
||||
|
||||
class Device:
|
||||
def __init__(self, dtype=None, memory="", labels=""):
|
||||
self._dtype = dtype
|
||||
self._memory = memory
|
||||
self._labels = labels
|
||||
|
||||
def __str__(self):
|
||||
return ",".join(self._labels)
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self._dtype
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return len(self._labels) or 1
|
||||
|
||||
@property
|
||||
def memory(self):
|
||||
return self._memory
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self._labels
|
||||
|
||||
@labels.setter
|
||||
def labels(self, lbs):
|
||||
if isinstance(lbs, str):
|
||||
self._labels = lbs.split(',')
|
||||
elif isinstance(lbs, list):
|
||||
self._labels = lbs
|
||||
else:
|
||||
self._labels = []
|
||||
|
||||
def get_selected_device_key(self):
|
||||
if self._dtype == DeviceType.CPU:
|
||||
return 'FLAGS_selected_cpus'
|
||||
if self._dtype == DeviceType.GPU:
|
||||
return 'FLAGS_selected_gpus'
|
||||
if self._dtype == DeviceType.XPU:
|
||||
return 'FLAGS_selected_xpus'
|
||||
if self._dtype == DeviceType.IPU:
|
||||
return 'FLAGS_selected_ipus'
|
||||
if self._dtype == DeviceType.CUSTOM_DEVICE:
|
||||
custom_device_types = get_all_custom_device_type()
|
||||
device_type = (
|
||||
str(custom_device_types[0]) if custom_device_types else ""
|
||||
)
|
||||
return f'FLAGS_selected_{device_type}s'
|
||||
return 'FLAGS_selected_devices'
|
||||
|
||||
def get_selected_devices(self, devices=''):
|
||||
'''
|
||||
return the device label/id relative to the visible devices
|
||||
'''
|
||||
if not devices:
|
||||
return [str(x) for x in range(0, len(self._labels))]
|
||||
else:
|
||||
devs = [x.strip() for x in devices.split(',')]
|
||||
return [str(self._labels.index(d)) for d in devs]
|
||||
|
||||
def get_custom_device_envs(self):
|
||||
custom_device_types = get_all_custom_device_type()
|
||||
device_type = str(custom_device_types[0]) if custom_device_types else ""
|
||||
return {
|
||||
'PADDLE_DISTRI_BACKEND': 'xccl',
|
||||
'PADDLE_XCCL_BACKEND': device_type,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def parse_device(self):
|
||||
dev = Device()
|
||||
visible_devices = None
|
||||
custom_device_types = get_all_custom_device_type()
|
||||
if custom_device_types:
|
||||
dev._dtype = DeviceType.CUSTOM_DEVICE
|
||||
device_type = str(custom_device_types[0])
|
||||
visible_devices_str = f'{device_type.upper()}_VISIBLE_DEVICES'
|
||||
if visible_devices_str in os.environ:
|
||||
visible_devices = os.getenv(visible_devices_str)
|
||||
elif 'XPULINK_VISIBLE_DEVICES' in os.environ:
|
||||
dev._dtype = DeviceType.XPU
|
||||
visible_devices = os.getenv("XPULINK_VISIBLE_DEVICES")
|
||||
elif 'XPU_VISIBLE_DEVICES' in os.environ:
|
||||
dev._dtype = DeviceType.XPU
|
||||
visible_devices = os.getenv("XPU_VISIBLE_DEVICES")
|
||||
elif 'CUDA_VISIBLE_DEVICES' in os.environ:
|
||||
if core.is_compiled_with_xpu():
|
||||
dev._dtype = DeviceType.XPU
|
||||
else:
|
||||
dev._dtype = DeviceType.GPU
|
||||
visible_devices = os.getenv("CUDA_VISIBLE_DEVICES")
|
||||
|
||||
if visible_devices is not None and visible_devices != 'all':
|
||||
dev._labels = visible_devices.split(',')
|
||||
else:
|
||||
return self.detect_device()
|
||||
|
||||
return dev
|
||||
|
||||
@classmethod
|
||||
def detect_device(self):
|
||||
def get_custom_devices_count(device_type):
|
||||
all_custom_devices = get_available_custom_device()
|
||||
all_custom_devices = [
|
||||
device.split(':')[0] for device in all_custom_devices
|
||||
]
|
||||
custom_devices_count = all_custom_devices.count(device_type)
|
||||
return custom_devices_count
|
||||
|
||||
dev = Device()
|
||||
num = 0
|
||||
visible_devices = None
|
||||
custom_device_types = get_all_custom_device_type()
|
||||
if custom_device_types:
|
||||
custom_device_type = str(custom_device_types[0])
|
||||
dev._dtype = DeviceType.CUSTOM_DEVICE
|
||||
num = get_custom_devices_count(custom_device_type)
|
||||
visible_devices_str = (
|
||||
f'{custom_device_type.upper()}_VISIBLE_DEVICES'
|
||||
)
|
||||
if visible_devices_str in os.environ:
|
||||
visible_devices = os.getenv(visible_devices_str)
|
||||
elif core.is_compiled_with_cuda():
|
||||
dev._dtype = DeviceType.GPU
|
||||
num = core.get_cuda_device_count()
|
||||
visible_devices = os.getenv("CUDA_VISIBLE_DEVICES")
|
||||
elif core.is_compiled_with_xpu():
|
||||
dev._dtype = DeviceType.XPU
|
||||
num = core.get_xpu_device_count()
|
||||
visible_devices = os.getenv("XPU_VISIBLE_DEVICES")
|
||||
elif core.is_compiled_with_ipu():
|
||||
dev._dtype = DeviceType.IPU
|
||||
num = core.get_ipu_device_count()
|
||||
# For IPUs, 'labels' is a list which contains the available numbers of IPU devices.
|
||||
dev._labels = [str(x) for x in range(0, num + 1)]
|
||||
return dev
|
||||
|
||||
if num == 0:
|
||||
dev._dtype = DeviceType.CPU
|
||||
elif visible_devices is None or visible_devices == "all":
|
||||
dev._labels = [str(x) for x in range(0, num)]
|
||||
else:
|
||||
dev._labels = visible_devices.split(',')
|
||||
|
||||
return dev
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
d = Device.parse_device()
|
||||
print(d.get_selected_devices())
|
||||
@@ -0,0 +1,20 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, kind="status", message="", fatal=False):
|
||||
self.kind = kind
|
||||
self.message = message
|
||||
self.fatal = fatal
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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 random
|
||||
import socket
|
||||
import struct
|
||||
from contextlib import closing
|
||||
|
||||
from .device import Device
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self):
|
||||
# self.device = Device.detect_device()
|
||||
self.device = Device.parse_device()
|
||||
self.ip = self.get_host_ip()
|
||||
self.free_ports = []
|
||||
self._allocated_ports = []
|
||||
|
||||
port_range = os.getenv('PORT_RANGE', '35100:64000')
|
||||
port_range = port_range.split(':')
|
||||
self._port_start = int(port_range[0])
|
||||
self._port_end = int(port_range[1])
|
||||
self._port_cur = random.randint(self._port_start, self._port_end)
|
||||
|
||||
def get_host_ip(self):
|
||||
try:
|
||||
self.hostname = socket.gethostname()
|
||||
self.ip = socket.gethostbyname(socket.getfqdn(self.hostname))
|
||||
return self.ip
|
||||
except:
|
||||
return '127.0.0.1'
|
||||
|
||||
def get_free_ports(self, n=1, rank=0):
|
||||
if os.environ.get('FLAGS_FIXED_PORT') is None:
|
||||
free_ports = [self.get_free_port() for i in range(n)]
|
||||
self.free_ports += free_ports
|
||||
else:
|
||||
start_port = int(os.environ.get('FLAGS_FIXED_PORT'))
|
||||
free_ports = list(
|
||||
range(start_port + rank, start_port + rank + n, 1)
|
||||
)
|
||||
self.free_ports += free_ports
|
||||
return free_ports
|
||||
|
||||
def get_ports_occupied(self):
|
||||
return self.free_ports
|
||||
|
||||
def _get_free_port(self, port=0):
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.setsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)
|
||||
)
|
||||
try:
|
||||
s.bind(('', port))
|
||||
return s.getsockname()[1]
|
||||
except:
|
||||
return -1
|
||||
|
||||
def _update_port_cur(self):
|
||||
self._port_cur += 1
|
||||
if self._port_cur > self._port_end:
|
||||
self._port_cur = self._port_start
|
||||
|
||||
def get_free_port(self):
|
||||
for _ in range(100):
|
||||
ret = self._get_free_port(self._port_cur)
|
||||
if ret > 0:
|
||||
self._update_port_cur()
|
||||
return ret
|
||||
else:
|
||||
self._update_port_cur()
|
||||
|
||||
return self._port_cur
|
||||
|
||||
@classmethod
|
||||
def is_server_ready(self, ip, port):
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||
# sock.settimeout(0.01)
|
||||
# sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, 'SO_REUSEPORT'):
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
result = sock.connect_ex((ip, int(port)))
|
||||
if result == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class Resource:
|
||||
def __init__(self):
|
||||
self.devices = []
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class Status:
|
||||
UNINIT = "uninit"
|
||||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
FAILED = "failed"
|
||||
TERMINATING = "terminating"
|
||||
RESTARTING = "restarting"
|
||||
UNKNOWN = "unknown"
|
||||
COMPLETED = "completed"
|
||||
DONE = "done" # should exit whatever status
|
||||
|
||||
def __init__(self):
|
||||
self._current_status = None
|
||||
|
||||
def current(self):
|
||||
return self._current_status
|
||||
|
||||
def is_running(self):
|
||||
return self._current_status == self.RUNNING
|
||||
|
||||
def is_restarting(self):
|
||||
return self._current_status == self.RESTARTING
|
||||
|
||||
def is_done(self):
|
||||
if self._current_status in [self.DONE, self.COMPLETED, self.FAILED]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
self._current_status = self.RUNNING
|
||||
|
||||
def fail(self):
|
||||
self._current_status = self.FAILED
|
||||
|
||||
def complete(self):
|
||||
self._current_status = self.COMPLETED
|
||||
|
||||
def restart(self):
|
||||
self._current_status = self.RESTARTING
|
||||
|
||||
def done(self):
|
||||
self._current_status = self.DONE
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,211 @@
|
||||
# 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 sys
|
||||
|
||||
from paddle.distributed.launch.utils.process_context import ProcessContext
|
||||
|
||||
from .status import Status
|
||||
|
||||
|
||||
class Container:
|
||||
'''
|
||||
TODO(kuizhiqing) A container can be run by process/thread or just a callable function
|
||||
'''
|
||||
|
||||
def __init__(self, entrypoint=[], rank=-1, env={}, overwrite_log=False):
|
||||
self._entrypoint = entrypoint
|
||||
self._rank = rank
|
||||
self._out = None
|
||||
self._err = None
|
||||
self._env = env
|
||||
self._proc = None
|
||||
|
||||
self._retry: int = 3
|
||||
self._grace_period = 10
|
||||
|
||||
self._log_handler = None
|
||||
self._shell = False
|
||||
|
||||
self.log_mode = 'w' if overwrite_log else 'a'
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
return self._env
|
||||
|
||||
@property
|
||||
def entrypoint(self):
|
||||
return self._entrypoint
|
||||
|
||||
@entrypoint.setter
|
||||
def entrypoint(self, entry):
|
||||
self._entrypoint = entry
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
return self._rank
|
||||
|
||||
@rank.setter
|
||||
def rank(self, r):
|
||||
self._rank = r
|
||||
|
||||
@property
|
||||
def outfile(self):
|
||||
return self._out
|
||||
|
||||
@outfile.setter
|
||||
def outfile(self, out):
|
||||
self._out = out
|
||||
|
||||
@property
|
||||
def errfile(self):
|
||||
return self._err
|
||||
|
||||
@errfile.setter
|
||||
def errfile(self, err):
|
||||
self._err = err
|
||||
|
||||
@property
|
||||
def shell(self):
|
||||
return self._shell
|
||||
|
||||
@shell.setter
|
||||
def shell(self, shell):
|
||||
self._shell = shell
|
||||
|
||||
def update_env(self, env={}, **kwargs):
|
||||
env = {k: v for k, v in env.items() if isinstance(v, str)}
|
||||
self._env.update(env)
|
||||
|
||||
kwargs = {k: v for k, v in kwargs.items() if isinstance(v, str)}
|
||||
self._env.update(kwargs)
|
||||
|
||||
def _validate_env(self):
|
||||
for k, v in self._env.items():
|
||||
assert isinstance(k, str) and isinstance(v, str), (
|
||||
f'env {k}:{v} must be str'
|
||||
)
|
||||
|
||||
def _get_fd(self, pth):
|
||||
if not pth:
|
||||
return None
|
||||
|
||||
try:
|
||||
d = os.path.dirname(pth)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return open(pth, self.log_mode)
|
||||
except:
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
if self._proc and self._proc.alive():
|
||||
return True
|
||||
|
||||
self._validate_env()
|
||||
|
||||
self._stdout = self._get_fd(self._out) or sys.stdout
|
||||
if self._out == self._err:
|
||||
self._stderr = self._stdout
|
||||
elif self._err:
|
||||
self._stderr = self._get_fd(self._err) or sys.stderr
|
||||
|
||||
if self._out and not self._log_handler:
|
||||
self._log_handler = open(self._out)
|
||||
self._log_handler.seek(0, 2)
|
||||
self._log_start_offset = self._log_handler.tell()
|
||||
|
||||
self._proc = ProcessContext(
|
||||
self._entrypoint,
|
||||
env=self._env,
|
||||
out=self._stdout,
|
||||
err=self._stderr,
|
||||
shell=self._shell,
|
||||
)
|
||||
|
||||
self._proc.start()
|
||||
|
||||
def terminate(self, force=False):
|
||||
if self._log_handler:
|
||||
self._log_handler.close()
|
||||
self._log_handler = None
|
||||
|
||||
if self._proc and self._proc.alive():
|
||||
return self._proc.terminate(force)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
try:
|
||||
self._proc.wait(timeout)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def exit_code(self):
|
||||
return self._proc.exit_code() if self._proc else -1
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if not self._proc:
|
||||
return Status.UNINIT
|
||||
if self._proc.alive():
|
||||
return Status.RUNNING
|
||||
elif self._proc.exit_code() == 0:
|
||||
return Status.COMPLETED
|
||||
else:
|
||||
return Status.FAILED
|
||||
|
||||
def __str__(self):
|
||||
need_print = os.environ.get('FLAGS_print_launcher_env', 'false').lower()
|
||||
if need_print == 'true' or need_print == '1':
|
||||
return f'Container rank {self._rank} status {self.status} cmd {self._entrypoint} code {self.exit_code} log {self.errfile} \nenv {self._env}'
|
||||
return f'Container rank {self._rank} status {self.status} cmd {self._entrypoint} code {self.exit_code} log {self.errfile}'
|
||||
|
||||
def logs(self, fn=None, offset=0, whence=1, limit=1000):
|
||||
if not self._log_handler:
|
||||
return
|
||||
|
||||
if fn is None:
|
||||
fn = sys.stdout
|
||||
|
||||
try:
|
||||
if offset != 0 or whence != 1:
|
||||
if whence == 0 and offset < self._log_start_offset:
|
||||
offset = self._log_start_offset
|
||||
self._log_handler.seek(offset, whence)
|
||||
|
||||
for _ in range(limit):
|
||||
line = self._log_handler.readline()
|
||||
if not line:
|
||||
return False
|
||||
fn.write(line)
|
||||
return True
|
||||
except:
|
||||
return
|
||||
|
||||
def tail(self, length=3000):
|
||||
if not self._log_handler:
|
||||
return
|
||||
|
||||
try:
|
||||
self._log_handler.seek(0, 2)
|
||||
ed = self._log_handler.tell()
|
||||
except:
|
||||
pass
|
||||
|
||||
if ed > length:
|
||||
self.logs(offset=ed - length, whence=0)
|
||||
else:
|
||||
self.logs(offset=0, whence=0)
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class JobMode:
|
||||
COLLECTIVE = 'collective'
|
||||
PS = 'ps'
|
||||
HETER = 'heter'
|
||||
|
||||
|
||||
class Job:
|
||||
def __init__(self, jid='default', mode=JobMode.COLLECTIVE, nnodes="1"):
|
||||
self._mode = mode
|
||||
self._id = jid
|
||||
|
||||
self._replicas = 0
|
||||
self._replicas_min = self._replicas
|
||||
self._replicas_max = self._replicas
|
||||
self._elastic = False
|
||||
|
||||
self.set_replicas(str(nnodes))
|
||||
|
||||
def __str__(self):
|
||||
return f"Job: {self.id}, mode {self.mode}, replicas {self._replicas}[{self._replicas_min}:{self._replicas_max}], elastic {self.elastic}"
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def elastic(self):
|
||||
return self._elastic
|
||||
|
||||
@property
|
||||
def replicas(self):
|
||||
return self._replicas
|
||||
|
||||
@property
|
||||
def replicas_min(self):
|
||||
return self._replicas_min
|
||||
|
||||
@property
|
||||
def replicas_max(self):
|
||||
return self._replicas_max
|
||||
|
||||
@replicas.setter
|
||||
def replicas(self, replicas):
|
||||
self._replicas = replicas
|
||||
|
||||
def set_replicas(self, nnodes: str):
|
||||
np = str(nnodes) if nnodes else '1'
|
||||
|
||||
if ':' in np:
|
||||
nps = np.split(':')
|
||||
self._replicas_min, self._replicas_max = int(nps[0]), int(nps[1])
|
||||
self._replicas = self._replicas_max # default to max
|
||||
|
||||
self._elastic = True
|
||||
else:
|
||||
self._replicas = int(np)
|
||||
self._replicas_min, self._replicas_max = (
|
||||
self._replicas,
|
||||
self._replicas,
|
||||
)
|
||||
|
||||
self._elastic = False
|
||||
@@ -0,0 +1,212 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .status import Status
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .container import Container
|
||||
|
||||
|
||||
class PodSpec:
|
||||
def __init__(self):
|
||||
self._name = ''.join(
|
||||
random.choice('abcdefghijklmnopqrstuvwxyz') for _ in range(6)
|
||||
)
|
||||
|
||||
# by controller
|
||||
self._init_containers: list[Container] = []
|
||||
self._containers: list[Container] = []
|
||||
|
||||
# self.resource: Resource = None
|
||||
# self.status: Status = None
|
||||
|
||||
self._rank = -1
|
||||
self._init_timeout = None
|
||||
self._restart = -1
|
||||
self._replicas = 0 # number of containers
|
||||
self._exit_code = 0
|
||||
|
||||
|
||||
class Pod(PodSpec):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Pod: {self.name}, replicas {self.replicas}, status {self.status}"
|
||||
)
|
||||
|
||||
def failed_container(self):
|
||||
cs = []
|
||||
for c in self._containers:
|
||||
if c.status == Status.FAILED:
|
||||
cs.append(c)
|
||||
return cs
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def replicas(self):
|
||||
return self._replicas
|
||||
|
||||
@replicas.setter
|
||||
def replicas(self, r):
|
||||
self._replicas = max(r, 1)
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
return self._rank
|
||||
|
||||
@rank.setter
|
||||
def rank(self, r):
|
||||
self._rank = r
|
||||
|
||||
@property
|
||||
def restart(self):
|
||||
return self._restart
|
||||
|
||||
@property
|
||||
def containers(self):
|
||||
return self._containers
|
||||
|
||||
def add_container(self, c):
|
||||
c.rank = len(self._containers)
|
||||
self._containers.append(c)
|
||||
|
||||
@property
|
||||
def init_containers(self):
|
||||
return self._init_containers
|
||||
|
||||
def add_init_container(self, c):
|
||||
c.rank = len(self._init_containers)
|
||||
self._init_containers.append(c)
|
||||
|
||||
@property
|
||||
def exit_code(self):
|
||||
for c in self._containers:
|
||||
if c.exit_code != 0:
|
||||
return c.exit_code
|
||||
return 0
|
||||
|
||||
def deploy(self):
|
||||
# init container should stop before run containers
|
||||
for i in self._init_containers:
|
||||
i.start()
|
||||
i.wait(self._init_timeout)
|
||||
for c in self._containers:
|
||||
c.start()
|
||||
|
||||
self._restart += 1
|
||||
|
||||
def stop(self, sigint=15, timeout=None):
|
||||
for c in self._containers:
|
||||
if isinstance(sigint, int) and timeout is None:
|
||||
c.send_signal(sigint)
|
||||
else:
|
||||
c.terminate()
|
||||
|
||||
if isinstance(timeout, int):
|
||||
if not self.join(timeout):
|
||||
for c in self._containers:
|
||||
c.terminate(force=True)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def join(self, timeout=None):
|
||||
for c in self._containers:
|
||||
if not c.wait(timeout):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if self.is_failed():
|
||||
return Status.FAILED
|
||||
|
||||
if self.is_completed():
|
||||
return Status.COMPLETED
|
||||
|
||||
if self.is_running():
|
||||
return Status.RUNNING
|
||||
|
||||
return Status.READY
|
||||
|
||||
def reset(self):
|
||||
self._init_containers = []
|
||||
self._containers = []
|
||||
|
||||
def is_failed(self):
|
||||
for c in self._containers:
|
||||
if c.status == Status.FAILED:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_completed(self):
|
||||
for c in self._containers:
|
||||
if c.status != Status.COMPLETED:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_running(self):
|
||||
for c in self._containers:
|
||||
if c.status != Status.RUNNING:
|
||||
return False
|
||||
return True
|
||||
|
||||
def logs(self, idx=None):
|
||||
if idx is None:
|
||||
if len(self._containers) > 0:
|
||||
self._containers[0].logs()
|
||||
if len(self._init_containers) > 0:
|
||||
self._init_containers[0].logs()
|
||||
else:
|
||||
self._containers[idx].logs()
|
||||
|
||||
def tail(self, idx=None):
|
||||
if idx is None:
|
||||
self._containers[0].tail()
|
||||
else:
|
||||
self._containers[idx].tail()
|
||||
|
||||
def watch(
|
||||
self,
|
||||
all_list=[Status.COMPLETED],
|
||||
any_list=[Status.FAILED],
|
||||
interval=1,
|
||||
timeout=-1,
|
||||
):
|
||||
'''
|
||||
watch return if any container status in any_list
|
||||
or all container status in all_list
|
||||
'''
|
||||
end = time.time() + timeout
|
||||
while timeout < 0 or time.time() < end:
|
||||
for c in self._init_containers + self._containers:
|
||||
if c.status in any_list:
|
||||
return c.status
|
||||
|
||||
s = [c.status for c in self._init_containers + self._containers]
|
||||
if len(set(s)) == 1 and s[0] in all_list:
|
||||
return s[0]
|
||||
|
||||
time.sleep(interval)
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class Status:
|
||||
UNINIT = "uninit"
|
||||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
FAILED = "failed"
|
||||
TERMINATING = "terminating"
|
||||
RESTARTING = "restarting"
|
||||
UNKNOWN = "unknown"
|
||||
COMPLETED = "completed"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
# print configuration after args are well filled in controller init
|
||||
def log(ctx):
|
||||
ctx.logger.info("----------- Configuration ----------------------")
|
||||
for arg, value in sorted(vars(ctx.args).items()):
|
||||
ctx.logger.info(f"{arg}: {value}")
|
||||
ctx.logger.info("--------------------------------------------------")
|
||||
|
||||
|
||||
def process_args(ctx):
|
||||
# reset device by args
|
||||
# argdev = ctx.args.gpus or ctx.args.xpus or ctx.args.npus
|
||||
argdev = ctx.args.devices
|
||||
if argdev:
|
||||
for d in argdev.split(','):
|
||||
if d not in ctx.node.device.labels:
|
||||
ctx.logger.error(
|
||||
f'Device not found {d} from {argdev} for setting {ctx.node.device.labels}'
|
||||
)
|
||||
|
||||
if ctx.args.ips:
|
||||
ips = ctx.args.ips.split(',')
|
||||
if '127.0.0.1' in ips and len(ips) != 1:
|
||||
raise ValueError("127.0.0.1 in ips is not allowed in multi-nodes.")
|
||||
|
||||
|
||||
def collective_compatible(ctx):
|
||||
force_use_args = int(os.getenv("PADDLE_LAUNCH_WITH_ARGS", "0"))
|
||||
if 'PADDLE_TRAINER_ENDPOINTS' in ctx.envs:
|
||||
eps = ctx.envs['PADDLE_TRAINER_ENDPOINTS'].split(',')
|
||||
hosts = {h.split(':')[0] for h in eps}
|
||||
if force_use_args:
|
||||
ctx.args.master = None
|
||||
else:
|
||||
ctx.args.master = eps[0] if ':' in eps[0] else f'{eps[0]}:6768'
|
||||
ctx.args.nnodes = len(hosts)
|
||||
ctx.logger.info(f'args reset by env PADDLE_TRAINER_ENDPOINTS\n{eps}')
|
||||
|
||||
if 'DISTRIBUTED_TRAINER_ENDPOINTS' in ctx.envs:
|
||||
eps = ctx.envs['DISTRIBUTED_TRAINER_ENDPOINTS'].split(',')
|
||||
hosts = {h.split(':')[0] for h in eps}
|
||||
if force_use_args:
|
||||
ctx.args.master = None
|
||||
else:
|
||||
ctx.args.master = eps[0]
|
||||
ctx.args.nnodes = len(hosts)
|
||||
ctx.logger.info(
|
||||
f'args reset by env DISTRIBUTED_TRAINER_ENDPOINTS\n{eps}'
|
||||
)
|
||||
|
||||
|
||||
def rewrite_host_ip(ctx):
|
||||
if ctx.args.host is not None and "." in ctx.args.host:
|
||||
ctx.logger.warning(f'Host ip reset to {ctx.args.host}')
|
||||
ctx.node.ip = ctx.args.host
|
||||
|
||||
|
||||
def test_mode(ctx):
|
||||
if ctx.args.training_script == 'run_check':
|
||||
ctx.logger.info('Paddle Distributed Test begin...')
|
||||
if int(ctx.args.nnodes) < 2:
|
||||
ctx.args.nnodes = 2
|
||||
ctx.args.training_script = f'{os.path.dirname(__file__)}/test.py'
|
||||
|
||||
|
||||
enabled_plugins = [
|
||||
test_mode,
|
||||
collective_compatible,
|
||||
rewrite_host_ip,
|
||||
process_args,
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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 numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.distributed import fleet
|
||||
from paddle.io import DataLoader, Dataset
|
||||
from paddle.vision.models import ResNet
|
||||
from paddle.vision.models.resnet import BottleneckBlock
|
||||
|
||||
base_lr = 0.1
|
||||
momentum_rate = 0.9
|
||||
l2_decay = 1e-4
|
||||
|
||||
epoch = 3
|
||||
batch_num = 1
|
||||
batch_size = 1
|
||||
class_dim = 102
|
||||
|
||||
|
||||
# define a random dataset
|
||||
class RandomDataset(Dataset):
|
||||
def __init__(self, num_samples):
|
||||
self.num_samples = num_samples
|
||||
|
||||
def __getitem__(self, idx):
|
||||
image = np.random.random([3, 224, 224]).astype('float32')
|
||||
label = np.random.randint(0, class_dim - 1, (1,)).astype('int64')
|
||||
return image, label
|
||||
|
||||
def __len__(self):
|
||||
return self.num_samples
|
||||
|
||||
|
||||
def optimizer_setting(parameter_list=None):
|
||||
optimizer = paddle.optimizer.Momentum(
|
||||
learning_rate=base_lr,
|
||||
momentum=momentum_rate,
|
||||
weight_decay=paddle.regularizer.L2Decay(l2_decay),
|
||||
parameters=parameter_list,
|
||||
)
|
||||
return optimizer
|
||||
|
||||
|
||||
def train_resnet():
|
||||
fleet.init(is_collective=True)
|
||||
|
||||
resnet = ResNet(BottleneckBlock, 18, num_classes=class_dim)
|
||||
optimizer = optimizer_setting(parameter_list=resnet.parameters())
|
||||
optimizer = fleet.distributed_optimizer(optimizer)
|
||||
resnet = fleet.distributed_model(resnet)
|
||||
|
||||
dataset = RandomDataset(batch_num * batch_size)
|
||||
train_loader = DataLoader(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
drop_last=True,
|
||||
num_workers=2,
|
||||
)
|
||||
|
||||
print("Distributed training start...")
|
||||
for eop in range(epoch):
|
||||
resnet.train()
|
||||
|
||||
for batch_id, data in enumerate(train_loader()):
|
||||
img, label = data
|
||||
label.stop_gradient = True
|
||||
|
||||
out = resnet(img)
|
||||
loss = paddle.nn.functional.cross_entropy(input=out, label=label)
|
||||
avg_loss = paddle.mean(x=loss)
|
||||
acc_top1 = paddle.metric.accuracy(input=out, label=label, k=1)
|
||||
acc_top5 = paddle.metric.accuracy(input=out, label=label, k=5)
|
||||
|
||||
avg_loss.backward()
|
||||
optimizer.step()
|
||||
resnet.clear_gradients()
|
||||
|
||||
print(
|
||||
f"[Epoch {eop}, batch {batch_id}] loss: {avg_loss:.5f}, acc1: {acc_top1:.5f}, acc5: {acc_top5:.5f}"
|
||||
)
|
||||
|
||||
print("Distributed training completed")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os
|
||||
|
||||
nnodes = os.getenv('PADDLE_NNODES')
|
||||
cn = os.getenv('PADDLE_LOCAL_SIZE')
|
||||
print(f"Prepare distributed training with {nnodes} nodes {cn} cards")
|
||||
train_resnet()
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) 2023 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 logging
|
||||
import time
|
||||
|
||||
import etcd3
|
||||
|
||||
|
||||
class ETCDClient:
|
||||
def __init__(self, host, port, retry_times=20):
|
||||
self.retry_times = retry_times
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
self.client = etcd3.client(host=host, port=port)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Initialize etcd client failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Initialize etcd client failed failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def put(self, key, value, lease=None, prev_kv=False):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.put(key, value, lease, prev_kv)
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Put failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(f"Put failed after {self.retry_times} times.")
|
||||
|
||||
def get(self, key):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.get(key)
|
||||
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Get {key} failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Get {key} failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def delete(self, key, prev_kv=False, return_response=False):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.delete(key, prev_kv, return_response)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Delete {key} failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Delete {key} failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def get_prefix(self, key_prefix, sort_order=None, sort_target='key'):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.get_prefix(key_prefix)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Get prefix {key_prefix} failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Get prefix {key_prefix} failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def delete_prefix(self, prefix):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.delete_prefix(prefix)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Delete prefix {prefix} failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Delete prefix {prefix} failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def lease(self, ttl, lease_id=None):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.lease(ttl, lease_id)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Lease failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(f"Lease failed after {self.retry_times} times.")
|
||||
|
||||
def add_watch_prefix_callback(self, key_prefix, callback, **kwargs):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.add_watch_prefix_callback(
|
||||
key_prefix, callback, **kwargs
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Add watch prefix callback failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Add watch prefix callback failed after {self.retry_times} times."
|
||||
)
|
||||
|
||||
def cancel_watch(self, watch_id):
|
||||
times = 0
|
||||
while times < self.retry_times:
|
||||
try:
|
||||
return self.client.cancel_watch(watch_id)
|
||||
break
|
||||
except Exception as e:
|
||||
times += 1
|
||||
logging.info(
|
||||
f"Cancel watch failed with exception {e}, retry after 1 second."
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
if times >= self.retry_times:
|
||||
raise ValueError(
|
||||
f"Cancel watch failed after {self.retry_times} times."
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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 time
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class KVClient:
|
||||
def __init__(self, endpoint='localhost:2379'):
|
||||
self.endpoint = (
|
||||
endpoint if endpoint.startswith("http://") else f"http://{endpoint}"
|
||||
)
|
||||
|
||||
def put(self, key, value):
|
||||
key = key if key.startswith('/') else f"/{key}"
|
||||
u = f"{self.endpoint}{key}"
|
||||
try:
|
||||
r = httpx.post(u, data=value, timeout=None, follow_redirects=True)
|
||||
if r.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
def get(self, key):
|
||||
key = key if key.startswith('/') else f"/{key}"
|
||||
u = f"{self.endpoint}{key}"
|
||||
try:
|
||||
r = httpx.get(u, timeout=None, follow_redirects=True)
|
||||
if r.status_code == 200:
|
||||
ret = r.json()
|
||||
return ret.get(key, '')
|
||||
else:
|
||||
return "error"
|
||||
except:
|
||||
return ""
|
||||
|
||||
def get_prefix(self, key):
|
||||
key = key if key.startswith('/') else f"/{key}"
|
||||
u = f"{self.endpoint}{key}"
|
||||
try:
|
||||
r = httpx.get(u, timeout=None, follow_redirects=True)
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
except:
|
||||
return ""
|
||||
|
||||
def delete(self, key):
|
||||
key = key if key.startswith('/') else f"/{key}"
|
||||
u = f"{self.endpoint}{key}"
|
||||
try:
|
||||
r = httpx.delete(u, timeout=None, follow_redirects=True)
|
||||
if r.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
def wait_server_ready(self, timeout=3):
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
if self.get("/healthy") == "ok":
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli = KVClient("http://localhost:8090")
|
||||
data = {"/workers/1": "rank1", "/workers/2": "rank2"}
|
||||
for k, v in data.items():
|
||||
cli.put(k, v)
|
||||
x = cli.get_prefix("/workers")
|
||||
print(x)
|
||||
for k, v in data.items():
|
||||
assert x[k] == v
|
||||
|
||||
cli.put("key", "value")
|
||||
print(cli.get("key"))
|
||||
assert cli.get("key") == "value"
|
||||
cli.delete("key")
|
||||
print(cli.get("/key"))
|
||||
print(cli.get("/healthy"))
|
||||
assert cli.get("/healthy") == "ok"
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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 http.server as SimpleHTTPServer
|
||||
import json
|
||||
import threading
|
||||
from http.server import HTTPServer
|
||||
from multiprocessing import Process
|
||||
|
||||
from .topology import SingleNodeTopology
|
||||
|
||||
|
||||
class KVHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
with self.server.kv_lock:
|
||||
ret = {}
|
||||
for k, v in self.server.kv.items():
|
||||
if k.startswith(self.path):
|
||||
ret[k] = v.decode(encoding="utf-8")
|
||||
if ret:
|
||||
self.output(200, json.dumps(ret).encode("utf-8"))
|
||||
else:
|
||||
self.output(404)
|
||||
|
||||
def do_PUT(self):
|
||||
self.do_POST()
|
||||
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'] or 0)
|
||||
try:
|
||||
value = self.rfile.read(content_length)
|
||||
with self.server.kv_lock:
|
||||
self.server.kv[self.path] = value
|
||||
self.output(200)
|
||||
return
|
||||
except:
|
||||
self.output(500)
|
||||
|
||||
def do_DELETE(self):
|
||||
with self.server.kv_lock:
|
||||
if self.path in self.server.kv:
|
||||
del self.server.kv[self.path]
|
||||
self.output(200)
|
||||
else:
|
||||
self.output(404)
|
||||
|
||||
def output(self, code, value=''):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Length", len(value))
|
||||
self.send_header("Content-Type", "application/json; charset=utf8")
|
||||
self.end_headers()
|
||||
if value:
|
||||
self.wfile.write(value)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
|
||||
class KVServer(HTTPServer):
|
||||
def __init__(self, port):
|
||||
super().__init__(('', port), KVHandler)
|
||||
self.kv_lock = threading.Lock()
|
||||
self.kv = {'/healthy': b'ok'}
|
||||
self.port = port
|
||||
self.stopped = False
|
||||
self.started = False
|
||||
self.node_topo = None
|
||||
|
||||
def start(self):
|
||||
self.listen_thread = threading.Thread(target=self.serve_forever)
|
||||
self.listen_thread.start()
|
||||
self.started = True
|
||||
|
||||
def stop(self):
|
||||
self.shutdown()
|
||||
self.listen_thread.join()
|
||||
self.server_close()
|
||||
self.stopped = True
|
||||
|
||||
def get_topology(self):
|
||||
if self.node_topo is None:
|
||||
self.node_topo = SingleNodeTopology()
|
||||
self.node_topo.detect()
|
||||
return self.node_topo.json_object
|
||||
|
||||
|
||||
class PKVServer:
|
||||
def __init__(self, port):
|
||||
self._server = KVServer(port)
|
||||
|
||||
def start(self):
|
||||
self.proc = Process(target=self._server.start)
|
||||
self.proc.daemon = True
|
||||
self.proc.start()
|
||||
|
||||
def stop(self):
|
||||
self._server.stop()
|
||||
self.proc.join()
|
||||
|
||||
@property
|
||||
def started(self):
|
||||
return self._server.started
|
||||
|
||||
@property
|
||||
def stopped(self):
|
||||
return self._server.stopped
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# kv = PKVServer(8090)
|
||||
kv = KVServer(8090)
|
||||
kv.start()
|
||||
import time
|
||||
|
||||
# print("serve at 8090 for 600 s")
|
||||
|
||||
time.sleep(600)
|
||||
@@ -0,0 +1,256 @@
|
||||
# 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 re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.base import core
|
||||
|
||||
|
||||
class Info:
|
||||
def __repr__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
def json(self):
|
||||
return json.dumps(self.__dict__)
|
||||
|
||||
def dict(self):
|
||||
return self.__dict__
|
||||
|
||||
def str(self, keys=None):
|
||||
if keys is None:
|
||||
keys = self.__dict__.keys()
|
||||
|
||||
if isinstance(keys, str):
|
||||
keys = keys.split(',')
|
||||
|
||||
values = [str(self.__dict__.get(k, '')) for k in keys]
|
||||
return ",".join(values)
|
||||
|
||||
|
||||
def query_smi(query=None, query_type="gpu", index=None, dtype=None):
|
||||
"""
|
||||
query_type: gpu/compute
|
||||
"""
|
||||
|
||||
if not has_nvidia_smi():
|
||||
return []
|
||||
|
||||
cmd = ["nvidia-smi", "--format=csv,noheader,nounits"]
|
||||
if isinstance(query, list) and query_type == "gpu":
|
||||
cmd.extend(["--query-gpu={}".format(",".join(query))])
|
||||
elif isinstance(query, list) and query_type.startswith("compute"):
|
||||
cmd.extend(["--query-compute-apps={}".format(",".join(query))])
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(index, list) and len(index) > 0:
|
||||
cmd.extend(["--id={}".format(",".join(index))])
|
||||
if not isinstance(dtype, list) or len(dtype) != len(query):
|
||||
dtype = [str] * len(query)
|
||||
|
||||
output = subprocess.check_output(cmd, timeout=3)
|
||||
lines = output.decode("utf-8").split(os.linesep)
|
||||
ret = []
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
info = Info()
|
||||
for k, v, d in zip(query, line.split(", "), dtype):
|
||||
setattr(info, k.replace(".", "_"), d(v))
|
||||
ret.append(info)
|
||||
return ret
|
||||
|
||||
|
||||
def query_rocm_smi(query=None, index=None, dtype=None, mem=32150):
|
||||
if not has_rocm_smi():
|
||||
return []
|
||||
|
||||
cmd = ["rocm-smi"]
|
||||
|
||||
if not isinstance(dtype, list) or len(dtype) != len(query):
|
||||
dtype = [str] * len(query)
|
||||
|
||||
output = subprocess.check_output(cmd, timeout=3)
|
||||
lines = output.decode("utf-8").split(os.linesep)
|
||||
ret = []
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
if len(line.split()) != 8 or "DCU" in line.split():
|
||||
continue
|
||||
info = Info()
|
||||
line = line.split()
|
||||
line = [
|
||||
line[0],
|
||||
line[7][: len(line[7]) - 1],
|
||||
mem,
|
||||
mem * float(line[6][: len(line[6]) - 1]) / 100,
|
||||
mem - mem * float(line[6][: len(line[6]) - 1]) / 100,
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
]
|
||||
for k, v, d in zip(query, line, dtype):
|
||||
setattr(info, k.replace(".", "_"), d(v))
|
||||
ret.append(info)
|
||||
return ret
|
||||
|
||||
|
||||
def query_npu_smi(query=None, index=None, dtype=None):
|
||||
if not has_npu_smi():
|
||||
return []
|
||||
|
||||
cmd = ["npu-smi", "info"]
|
||||
|
||||
if not isinstance(dtype, list) or len(dtype) != len(query):
|
||||
dtype = [str] * len(query)
|
||||
|
||||
output = subprocess.check_output(cmd, timeout=3)
|
||||
lines = output.decode("utf-8").split(os.linesep)
|
||||
ret = []
|
||||
i = 0
|
||||
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
result = re.split(r',|/|\s+|\|', line)
|
||||
# result = [item for item in result if item]
|
||||
length = len(result)
|
||||
if length not in [18, 19] or "NPU" in result:
|
||||
continue
|
||||
result = [item for item in result if item]
|
||||
info = Info()
|
||||
result = [
|
||||
i,
|
||||
result[2],
|
||||
result[6],
|
||||
float(result[5]),
|
||||
(float(result[6]) - float(result[5])),
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
]
|
||||
i += 1
|
||||
for k, v, d in zip(query, result, dtype):
|
||||
setattr(info, k.replace(".", "_"), d(v))
|
||||
ret.append(info)
|
||||
return ret
|
||||
|
||||
|
||||
def query_xpu_smi(query=None, index=None, dtype=None):
|
||||
if (
|
||||
not hasattr(core, "get_xpu_device_count")
|
||||
or core.get_xpu_device_count() == 0
|
||||
):
|
||||
return []
|
||||
if not isinstance(dtype, list) or len(dtype) != len(query):
|
||||
dtype = [str] * len(query)
|
||||
if not isinstance(index, list) or len(index) == 0:
|
||||
index = list(range(core.get_xpu_device_count()))
|
||||
ret = []
|
||||
for dev_id in index:
|
||||
dev_id = int(dev_id)
|
||||
utilization_xpu = core.get_xpu_device_utilization_rate(dev_id)
|
||||
mem_total = (
|
||||
core.get_xpu_device_total_memory(dev_id) / 1024 / 1024
|
||||
) # with MB
|
||||
mem_used = (
|
||||
core.get_xpu_device_used_memory(dev_id) / 1024 / 1024
|
||||
) # with MB
|
||||
result = [
|
||||
dev_id,
|
||||
utilization_xpu,
|
||||
mem_total,
|
||||
mem_used,
|
||||
(mem_total - mem_used),
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
||||
]
|
||||
info = Info()
|
||||
for k, v, d in zip(query, result, dtype):
|
||||
setattr(info, k.replace(".", "_"), d(v))
|
||||
ret.append(info)
|
||||
return ret
|
||||
|
||||
|
||||
def get_gpu_info(index=None):
|
||||
q = "index,uuid,driver_version,name,gpu_serial,display_active,display_mode".split(
|
||||
","
|
||||
)
|
||||
d = [int, str, str, str, str, str, str]
|
||||
index = (
|
||||
index
|
||||
if index is None or isinstance(index, list)
|
||||
else str(index).split(",")
|
||||
)
|
||||
|
||||
return query_smi(q, index=index, dtype=d)
|
||||
|
||||
|
||||
def get_gpu_util(index=None):
|
||||
q = "index,utilization.gpu,memory.total,memory.used,memory.free,timestamp".split(
|
||||
","
|
||||
)
|
||||
d = [int, int, int, int, int, str]
|
||||
index = (
|
||||
index
|
||||
if index is None or isinstance(index, list)
|
||||
else str(index).split(",")
|
||||
)
|
||||
if paddle.device.is_compiled_with_rocm():
|
||||
return query_rocm_smi(q, index=index, dtype=d)
|
||||
elif paddle.device.is_compiled_with_custom_device('npu'):
|
||||
return query_npu_smi(q, index=index, dtype=d)
|
||||
elif paddle.is_compiled_with_xpu():
|
||||
return query_xpu_smi(q, index=index, dtype=d)
|
||||
return query_smi(q, index=index, dtype=d)
|
||||
|
||||
|
||||
def get_gpu_process(index=None):
|
||||
q = "pid,process_name,gpu_uuid,gpu_name,used_memory".split(",")
|
||||
d = [int, str, str, str, int]
|
||||
index = (
|
||||
index
|
||||
if index is None or isinstance(index, list)
|
||||
else str(index).split(",")
|
||||
)
|
||||
|
||||
return query_smi(q, index=index, query_type="compute", dtype=d)
|
||||
|
||||
|
||||
def has_nvidia_smi():
|
||||
return shutil.which("nvidia-smi")
|
||||
|
||||
|
||||
def has_rocm_smi():
|
||||
return shutil.which("rocm-smi")
|
||||
|
||||
|
||||
def has_npu_smi():
|
||||
return shutil.which("npu-smi")
|
||||
|
||||
|
||||
def has_xpu_smi():
|
||||
return shutil.which("xpu-smi")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(get_gpu_info(0))
|
||||
print(get_gpu_util(0))
|
||||
print(get_gpu_process(0))
|
||||
|
||||
u = get_gpu_util()
|
||||
for i in u:
|
||||
print(i.str())
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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 signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
LIMIT_LEN_ENVS = ["TRAINER_IP_PORT_LIST", "PADDLE_TRAINER_ENDPOINTS"]
|
||||
|
||||
|
||||
class ProcessContext:
|
||||
def __init__(
|
||||
self,
|
||||
cmd,
|
||||
env=os.environ,
|
||||
out=sys.stdout,
|
||||
err=sys.stderr,
|
||||
group=True,
|
||||
preexec_fn=None,
|
||||
shell=False,
|
||||
):
|
||||
self._cmd = cmd
|
||||
self._env = env
|
||||
self._preexec_fn = preexec_fn
|
||||
self._stdout = out
|
||||
self._stderr = err
|
||||
self._group = group if os.name != 'nt' else False
|
||||
self._proc = None
|
||||
self._code = None
|
||||
self._shell = shell
|
||||
|
||||
def _start(self):
|
||||
pre_fn = os.setsid if self._group else None
|
||||
log_dir = self._env["PADDLE_LOG_DIR"]
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
rank = self._env.get("PADDLE_TRAINER_ID")
|
||||
if rank is not None:
|
||||
rank = int(rank)
|
||||
backup_env_path = str(
|
||||
os.path.join(log_dir, f'backup_env.{rank}.json')
|
||||
)
|
||||
envs = {"PADDLE_BACKUP_ENV_PATH": backup_env_path}
|
||||
|
||||
max_len = int(os.getenv('PADDLE_ENV_LIMIT_LEN', 48000))
|
||||
for k, v in self._env.items():
|
||||
if k not in LIMIT_LEN_ENVS or len(v) < max_len:
|
||||
envs[k] = v
|
||||
|
||||
with open(backup_env_path, 'w') as f:
|
||||
json.dump(dict(self._env), f, indent=4, sort_keys=True)
|
||||
else:
|
||||
envs = self._env
|
||||
|
||||
self._proc = subprocess.Popen(
|
||||
self._cmd,
|
||||
env=envs,
|
||||
stdout=self._stdout,
|
||||
stderr=self._stderr,
|
||||
preexec_fn=self._preexec_fn or pre_fn,
|
||||
shell=self._shell,
|
||||
)
|
||||
|
||||
def _close_std(self):
|
||||
try:
|
||||
if not self._stdout.isatty():
|
||||
self._stdout.close()
|
||||
|
||||
if not self._stderr.isatty():
|
||||
self._stderr.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def alive(self):
|
||||
return self._proc and self._proc.poll() is None
|
||||
|
||||
def exit_code(self):
|
||||
return self._proc.poll() if self._proc else None
|
||||
|
||||
def start(self):
|
||||
self._start()
|
||||
|
||||
def terminate(self, force=False, max_retry=3):
|
||||
for i in range(max_retry):
|
||||
if self.alive():
|
||||
if self._group:
|
||||
os.killpg(os.getpgid(self._proc.pid), signal.SIGTERM)
|
||||
else:
|
||||
self._proc.terminate()
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
break
|
||||
|
||||
if force and self.alive():
|
||||
self._proc.kill()
|
||||
|
||||
self._close_std()
|
||||
|
||||
return self.alive()
|
||||
|
||||
def wait(self, timeout=None):
|
||||
self._proc.wait(timeout)
|
||||
@@ -0,0 +1,362 @@
|
||||
# Copyright (c) 2023 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 subprocess
|
||||
import warnings
|
||||
|
||||
import paddle
|
||||
|
||||
|
||||
def call_cmd(cmd, err_msg, default_value):
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
shell=True,
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
if stderr:
|
||||
warnings.warn(err_msg)
|
||||
stdout = default_value
|
||||
|
||||
return stdout
|
||||
|
||||
|
||||
class SingleNodeTopology:
|
||||
def __init__(self):
|
||||
self.pcie_latency = 0.0
|
||||
self.pcie_bandwidth = float('inf')
|
||||
self.nvlink_bandwidth = -1.0
|
||||
self.nb_devices = 8
|
||||
|
||||
self.machine = {}
|
||||
self.devices = []
|
||||
self.links = []
|
||||
self.json_object = None
|
||||
|
||||
def calculate_cpu_flops(self):
|
||||
# Get number sockets
|
||||
cmd = "lscpu | grep 'Socket(s)' | awk '{print $NF}'"
|
||||
err_msg = "Failed to get number of sockets"
|
||||
default_value = 4
|
||||
nb_sockets = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
# Get number of cores per socket
|
||||
cmd = "lscpu | grep 'Core(s) per socket' | awk '{print $NF}'"
|
||||
err_msg = "Failed to get number of cores per socket"
|
||||
default_value = 20
|
||||
nb_cores_per_socket = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
# Get clock speed
|
||||
cmd = "lscpu | grep GHz | awk -F '@' '{print $NF}' | awk -F 'G' '{print $1}'"
|
||||
err_msg = "Failed to get cpu clock rate"
|
||||
default_value = 2.4
|
||||
clock_rate = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
# Get number of FMA units
|
||||
# TODO(changtao02): find a way to detect this value
|
||||
nb_fmas = 2
|
||||
|
||||
# Get SIMD width
|
||||
simd_width_sp = 0
|
||||
simd_width_dp = 0
|
||||
|
||||
cmd = "lscpu | grep sse"
|
||||
err_msg = "Failed to get cpu vector size"
|
||||
default_value = "sse"
|
||||
vector_size = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
if vector_size:
|
||||
simd_width_sp = 4 # 128 / 32
|
||||
simd_width_dp = 2 # 128 / 64
|
||||
|
||||
cmd = "lscpu | grep avx2"
|
||||
err_msg = "Failed to get cpu vector size"
|
||||
default_value = "avx2"
|
||||
vector_size = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
if vector_size:
|
||||
simd_width_sp = 8 # 256 / 32
|
||||
simd_width_dp = 4 # 256 / 64
|
||||
|
||||
cmd = "lscpu | grep avx512"
|
||||
err_msg = "Failed to get cpu vector size"
|
||||
default_value = "avx512"
|
||||
vector_size = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
if vector_size:
|
||||
simd_width_sp = 16 # 512 / 32
|
||||
simd_width_dp = 8 # 512 / 64
|
||||
|
||||
gflops_per_element = (
|
||||
int(nb_sockets)
|
||||
* int(nb_cores_per_socket)
|
||||
* float(clock_rate)
|
||||
* nb_fmas
|
||||
)
|
||||
sp_gflops = gflops_per_element * simd_width_sp
|
||||
dp_gflops = gflops_per_element * simd_width_dp
|
||||
|
||||
self.machine['sp_gflops'] = sp_gflops
|
||||
self.machine['dp_gflops'] = dp_gflops
|
||||
|
||||
def pcie_gen2bandwidth(self, pcie_generation):
|
||||
if pcie_generation == 1:
|
||||
return 0.25
|
||||
elif pcie_generation == 2:
|
||||
return 0.5
|
||||
elif pcie_generation == 3:
|
||||
return 1.0
|
||||
elif pcie_generation == 4:
|
||||
return 2.0
|
||||
elif pcie_generation == 5:
|
||||
return 4.0
|
||||
elif pcie_generation == 6:
|
||||
return 8.0
|
||||
|
||||
def model2gflops(self, model):
|
||||
if "H100" in model and "SXM5" in model:
|
||||
return 60000, 30000
|
||||
elif "H100" in model and "PCIe" in model:
|
||||
return 48000, 24000
|
||||
elif "A100" in model:
|
||||
return 19500, 9700
|
||||
elif "A800" in model:
|
||||
return 19500, 9700
|
||||
elif "V100" in model:
|
||||
return 15700, 7800
|
||||
elif "P100" in model:
|
||||
return 10600, 5300
|
||||
|
||||
def get_link_bandwidth(self, source_id, target_id):
|
||||
# Get link type
|
||||
row_id = 2 + source_id
|
||||
column_id = 2 + target_id
|
||||
|
||||
cmd = (
|
||||
"cat /tmp/matrix.txt | awk 'FNR=="
|
||||
+ str(row_id)
|
||||
+ " {print $"
|
||||
+ str(column_id)
|
||||
+ "}'"
|
||||
)
|
||||
err_msg = "Failed to get topo matrix"
|
||||
default_value = "NVL"
|
||||
link_type = call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
link_bandwidth = self.pcie_bandwidth
|
||||
|
||||
if "NV" in link_type:
|
||||
if self.nvlink_bandwidth == -1.0:
|
||||
cmd = "nvidia-smi nvlink -s -i 0 | tail -n 1 | awk '{print $3}'"
|
||||
err_msg = "Failed to get nvlink bandwidth"
|
||||
default_value = "25"
|
||||
self.nvlink_bandwidth = float(
|
||||
call_cmd(cmd, err_msg, default_value)
|
||||
)
|
||||
|
||||
link_bandwidth = int(link_type[2:]) * self.nvlink_bandwidth
|
||||
link_type = "NVL"
|
||||
|
||||
return link_type, link_bandwidth
|
||||
|
||||
def get_host_info(self):
|
||||
# Get hostname
|
||||
cmd = "hostname -s"
|
||||
err_msg = "Failed to get hostname"
|
||||
default_value = "localhost"
|
||||
hostname = call_cmd(cmd, err_msg, default_value).strip()
|
||||
|
||||
# Get ip address
|
||||
cmd = "hostname -i"
|
||||
err_msg = "Failed to get host ip address"
|
||||
default_value = "127.0.0.1"
|
||||
ip_addr = call_cmd(cmd, err_msg, default_value).strip()
|
||||
|
||||
# Get CPU memory (GB)
|
||||
cmd = "cat /proc/meminfo | grep 'MemAvailable' | awk -F ':' '{print $NF}' | awk '{print $1}'"
|
||||
err_msg = "Failed to get cpu memory"
|
||||
default_value = "41366484"
|
||||
cpu_memory = int(call_cmd(cmd, err_msg, default_value)) // 1e6
|
||||
|
||||
# Get single-point flops and double-point flops (GFLOPs)
|
||||
self.calculate_cpu_flops()
|
||||
|
||||
self.machine['hostname'] = hostname
|
||||
self.machine['addr'] = ip_addr
|
||||
self.machine['memory'] = cpu_memory
|
||||
|
||||
def get_device_info(self):
|
||||
# Get device count
|
||||
cmd = "nvidia-smi -L | wc -l"
|
||||
err_msg = "Failed to get device count"
|
||||
default_value = "8"
|
||||
self.nb_devices = int(call_cmd(cmd, err_msg, default_value))
|
||||
|
||||
local_size = int(os.getenv("PADDLE_LOCAL_SIZE"))
|
||||
if local_size < self.nb_devices:
|
||||
self.nb_devices = local_size
|
||||
# Get PCIe latency and bandwidth (ms, GB/s)
|
||||
for i in range(self.nb_devices):
|
||||
cmd = (
|
||||
"nvidia-smi --id="
|
||||
+ str(i)
|
||||
+ " --query-gpu=pcie.link.gen.max --format=csv,noheader"
|
||||
)
|
||||
err_msg = "Failed to get max pcie link generation"
|
||||
default_value = "4"
|
||||
pcie_generation = int(call_cmd(cmd, err_msg, default_value))
|
||||
|
||||
cmd = (
|
||||
"nvidia-smi --id="
|
||||
+ str(i)
|
||||
+ " --query-gpu=pcie.link.width.max --format=csv,noheader"
|
||||
)
|
||||
err_msg = "Failed to get max pcie link width"
|
||||
default_value = "16"
|
||||
pcie_width = int(call_cmd(cmd, err_msg, default_value))
|
||||
|
||||
self.pcie_bandwidth = min(
|
||||
self.pcie_bandwidth,
|
||||
self.pcie_gen2bandwidth(pcie_generation) * pcie_width,
|
||||
)
|
||||
|
||||
dev_global_ids = []
|
||||
dev_local_ids = []
|
||||
dev_types = []
|
||||
dev_models = []
|
||||
dev_memories = [] # GiB
|
||||
dev_sp_gflops = [] # GB/s
|
||||
dev_dp_gflops = [] # GB/s
|
||||
|
||||
# Get device info
|
||||
rank_first = paddle.paddle.distributed.get_rank()
|
||||
for i in range(self.nb_devices):
|
||||
dev_global_ids.append(i + rank_first)
|
||||
dev_local_ids.append(i)
|
||||
dev_types.append("GPU")
|
||||
|
||||
cmd = (
|
||||
"nvidia-smi --id="
|
||||
+ str(i)
|
||||
+ " --query-gpu=name --format=csv,noheader"
|
||||
)
|
||||
err_msg = "Failed to get device name"
|
||||
default_value = "NVIDIA A100-SXM4-40GB"
|
||||
dev_models.append(call_cmd(cmd, err_msg, default_value).strip())
|
||||
|
||||
cmd = (
|
||||
"nvidia-smi --id="
|
||||
+ str(i)
|
||||
+ " --query-gpu=memory.free --format=csv,noheader | awk '{print $1}'"
|
||||
)
|
||||
err_msg = "Failed to get device available memory"
|
||||
default_value = "40536"
|
||||
dev_memories.append(
|
||||
int(call_cmd(cmd, err_msg, default_value)) // 1e3
|
||||
)
|
||||
|
||||
sp_gflops, dp_gflops = self.model2gflops(dev_models[i])
|
||||
dev_sp_gflops.append(sp_gflops)
|
||||
dev_dp_gflops.append(dp_gflops)
|
||||
|
||||
for i in range(len(dev_global_ids)):
|
||||
device = {}
|
||||
device['global_id'] = dev_global_ids[i]
|
||||
device['local_id'] = dev_local_ids[i]
|
||||
device['type'] = dev_types[i]
|
||||
device['model'] = dev_models[i]
|
||||
device['memory'] = dev_memories[i]
|
||||
device['sp_gflops'] = dev_sp_gflops[i]
|
||||
device['dp_gflops'] = dev_dp_gflops[i]
|
||||
self.devices.append(device)
|
||||
|
||||
self.machine['latency'] = self.pcie_latency
|
||||
self.machine['bandwidth'] = self.pcie_bandwidth
|
||||
self.machine['device_type'] = dev_types[0]
|
||||
self.machine['device_type_full'] = f"{dev_types[0]}-{dev_models[0]}"
|
||||
self.machine['devices'] = self.devices
|
||||
|
||||
def get_link_info(self):
|
||||
link_source_global_ids = []
|
||||
link_target_global_ids = []
|
||||
link_types = []
|
||||
link_latencies = [] # ms
|
||||
link_bandwidths = [] # GB/s
|
||||
|
||||
cmd = "nvidia-smi topo -m > /tmp/matrix.txt"
|
||||
err_msg = "Failed to get topo matrix"
|
||||
default_value = ""
|
||||
call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
rank_first = paddle.paddle.distributed.get_rank()
|
||||
# Get link info between devices
|
||||
for i in range(self.nb_devices):
|
||||
for j in range(self.nb_devices):
|
||||
if i == j:
|
||||
link_types.append("X")
|
||||
link_bandwidths.append(-1.0)
|
||||
else:
|
||||
link_source_global_ids.append(i + rank_first)
|
||||
link_target_global_ids.append(j + rank_first)
|
||||
link_latencies.append(0.0)
|
||||
if i > j:
|
||||
index = j * self.nb_devices + i
|
||||
link_types.append(link_types[index])
|
||||
link_bandwidths.append(link_bandwidths[index])
|
||||
elif i < j:
|
||||
link_type, link_bandwidth = self.get_link_bandwidth(
|
||||
i, j
|
||||
)
|
||||
link_types.append(link_type)
|
||||
link_bandwidths.append(link_bandwidth)
|
||||
|
||||
for i in reversed(range(self.nb_devices)):
|
||||
link_types.pop(i * self.nb_devices + i)
|
||||
link_bandwidths.pop(i * self.nb_devices + i)
|
||||
|
||||
cmd = "rm /tmp/matrix.txt"
|
||||
err_msg = "Failed to delete matrix.txt"
|
||||
default_value = ""
|
||||
# call_cmd(cmd, err_msg, default_value)
|
||||
|
||||
for i in range(len(link_types)):
|
||||
link = {}
|
||||
link['source_global_id'] = link_source_global_ids[i]
|
||||
link['target_global_id'] = link_target_global_ids[i]
|
||||
link['type'] = link_types[i]
|
||||
link['latency'] = link_latencies[i]
|
||||
link['bandwidth'] = link_bandwidths[i]
|
||||
self.links.append(link)
|
||||
|
||||
self.machine['links'] = self.links
|
||||
|
||||
def detect(self):
|
||||
# Get host info
|
||||
self.get_host_info()
|
||||
|
||||
# Get device info
|
||||
self.get_device_info()
|
||||
|
||||
# Get link info between devices
|
||||
self.get_link_info()
|
||||
|
||||
self.json_object = json.dumps(self.machine, indent=4)
|
||||
|
||||
def dump(self, output_path):
|
||||
with open(output_path, "w") as outfile:
|
||||
json.dump(self.machine, outfile, indent=4)
|
||||
Reference in New Issue
Block a user