chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user