libpipe 0.11__tar.gz → 0.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {libpipe-0.11 → libpipe-0.12}/PKG-INFO +3 -2
- {libpipe-0.11 → libpipe-0.12}/README.md +2 -1
- {libpipe-0.11 → libpipe-0.12}/libpipe/worker.py +72 -28
- {libpipe-0.11 → libpipe-0.12}/pyproject.toml +1 -1
- {libpipe-0.11 → libpipe-0.12}/LICENSE +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/__init__.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/attrmap.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/futils.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/msutils.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/settings.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/taskrun.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/tools/__init__.py +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/tools/libnodes_bash.sh +0 -0
- {libpipe-0.11 → libpipe-0.12}/libpipe/tools/nodetool.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libpipe
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12
|
|
4
4
|
Summary: Pipeline orchestration and node management utilities
|
|
5
5
|
License-Expression: GPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE
|
|
@@ -64,7 +64,8 @@ on a file system shared with the compute nodes.
|
|
|
64
64
|
|
|
65
65
|
With `slurm_nodes`, a pool on a login node requests one allocation of up to that many nodes
|
|
66
66
|
when its first task starts, runs each task as a job step in it (`srun --jobid --exact`), and
|
|
67
|
-
releases it at the end, or after 10 minutes without step.
|
|
67
|
+
releases it at the end, or after 10 minutes without step. `slurm_nodes='2-10'` starts it as
|
|
68
|
+
soon as 2 nodes are free, with up to 10 of them, as many as free at that moment. Slurm packs the steps on the nodes
|
|
68
69
|
from their CPUs and memory, given by the category of the task; a step starts when the
|
|
69
70
|
allocation has its CPUs free, so that no srun waits polling the controller:
|
|
70
71
|
|
|
@@ -30,7 +30,8 @@ on a file system shared with the compute nodes.
|
|
|
30
30
|
|
|
31
31
|
With `slurm_nodes`, a pool on a login node requests one allocation of up to that many nodes
|
|
32
32
|
when its first task starts, runs each task as a job step in it (`srun --jobid --exact`), and
|
|
33
|
-
releases it at the end, or after 10 minutes without step.
|
|
33
|
+
releases it at the end, or after 10 minutes without step. `slurm_nodes='2-10'` starts it as
|
|
34
|
+
soon as 2 nodes are free, with up to 10 of them, as many as free at that moment. Slurm packs the steps on the nodes
|
|
34
35
|
from their CPUs and memory, given by the category of the task; a step starts when the
|
|
35
36
|
allocation has its CPUs free, so that no srun waits polling the controller:
|
|
36
37
|
|
|
@@ -48,8 +48,9 @@ DEFAULT_RESOURCES = {
|
|
|
48
48
|
THREAD_VARS = ('OMP_NUM_THREADS', 'OPENBLAS_NUM_THREADS', 'MKL_NUM_THREADS')
|
|
49
49
|
# The holding job of an allocation ends after this many minutes without job step
|
|
50
50
|
ALLOC_IDLE_MINUTES = 10
|
|
51
|
-
#
|
|
52
|
-
|
|
51
|
+
# Final states of an allocation that stop the pool instead of requesting another one: each new
|
|
52
|
+
# allocation would land on other nodes, and suspend or end other jobs, for nothing
|
|
53
|
+
ALLOC_FAILED_STATES = ('NODE_FAIL', 'BOOT_FAIL', 'FAILED', 'OUT_OF_MEMORY')
|
|
53
54
|
# Launches of a task in an allocation, when srun ends before the command starts (the allocation
|
|
54
55
|
# ended, or is being replaced)
|
|
55
56
|
ALLOC_MAX_LAUNCHES = 3
|
|
@@ -57,6 +58,31 @@ ALLOC_MAX_LAUNCHES = 3
|
|
|
57
58
|
ALLOC_WORKERS_PER_NODE = 32
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
def node_range(slurm_nodes):
|
|
62
|
+
"""(min, max) nodes of an allocation from SLURM_NODES: N, or 'MIN-MAX' to start it once MIN
|
|
63
|
+
nodes are free, with up to MAX of them; None without (None, 0)."""
|
|
64
|
+
if slurm_nodes is None or slurm_nodes == 0:
|
|
65
|
+
return None
|
|
66
|
+
if isinstance(slurm_nodes, tuple):
|
|
67
|
+
lo, hi = slurm_nodes
|
|
68
|
+
elif isinstance(slurm_nodes, int) and not isinstance(slurm_nodes, bool):
|
|
69
|
+
lo = hi = slurm_nodes
|
|
70
|
+
elif isinstance(slurm_nodes, str) and re.fullmatch(r'\d+(-\d+)?', slurm_nodes.strip()):
|
|
71
|
+
lo, _, hi = slurm_nodes.strip().partition('-')
|
|
72
|
+
lo, hi = int(lo), int(hi or lo)
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(f'slurm_nodes: {slurm_nodes!r} is neither a number of nodes nor MIN-MAX')
|
|
75
|
+
if not 1 <= lo <= hi:
|
|
76
|
+
raise ValueError(f'slurm_nodes: {slurm_nodes!r}, MIN-MAX needs 1 <= MIN <= MAX')
|
|
77
|
+
return lo, hi
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def nodes_option(nodes):
|
|
81
|
+
"""The --nodes value of an allocation of (min, max) nodes."""
|
|
82
|
+
lo, hi = nodes
|
|
83
|
+
return f'{lo}-{hi}' if lo < hi else str(hi)
|
|
84
|
+
|
|
85
|
+
|
|
60
86
|
def now_str():
|
|
61
87
|
return datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')
|
|
62
88
|
|
|
@@ -560,10 +586,10 @@ SBATCH_TRANSIENT = ('timed out', 'temporarily', 'Unable to contact', 'Connection
|
|
|
560
586
|
|
|
561
587
|
class SlurmClient:
|
|
562
588
|
"""Run each task with srun: a Slurm job of its own when the pool runs on a login node, a
|
|
563
|
-
job step when it runs inside an allocation (salloc, sbatch). With alloc_nodes
|
|
564
|
-
requests an allocation of that many nodes and runs
|
|
565
|
-
its nodes from their CPUs and memory; the allocation
|
|
566
|
-
ALLOC_IDLE_MINUTES without step.
|
|
589
|
+
job step when it runs inside an allocation (salloc, sbatch). With alloc_nodes (N or
|
|
590
|
+
'MIN-MAX', see node_range), the client requests an allocation of that many nodes and runs
|
|
591
|
+
the tasks as job steps in it, packed on its nodes from their CPUs and memory; the allocation
|
|
592
|
+
ends with the pool, or after ALLOC_IDLE_MINUTES without step.
|
|
567
593
|
|
|
568
594
|
Options (partition, account, ...) come from slurm_options, or from the LIBPIPE_SLURM_OPTIONS
|
|
569
595
|
environment variable; they apply to the jobs and the allocation, not to the job steps.
|
|
@@ -580,9 +606,10 @@ class SlurmClient:
|
|
|
580
606
|
self.max_time = max_time
|
|
581
607
|
self.exact = False
|
|
582
608
|
self.steps = False
|
|
583
|
-
self.alloc_nodes = alloc_nodes
|
|
609
|
+
self.alloc_nodes = node_range(alloc_nodes)
|
|
584
610
|
self.alloc_id = None
|
|
585
611
|
self.alloc_refused = None
|
|
612
|
+
self.alloc_ran = False
|
|
586
613
|
self.alloc_checked = 0
|
|
587
614
|
self.alloc_lock = asyncio.Lock()
|
|
588
615
|
# CPUs of the allocation and those free: a step starts when its CPUs are free, instead of
|
|
@@ -683,33 +710,47 @@ class SlurmClient:
|
|
|
683
710
|
return process.returncode, out.decode().strip(), err.decode().strip()
|
|
684
711
|
|
|
685
712
|
async def ensure_allocation(self, check=False):
|
|
686
|
-
"""Request the allocation, or a new one when it has ended
|
|
687
|
-
|
|
713
|
+
"""Request the allocation, or a new one when it has ended after running normally (time
|
|
714
|
+
limit, idle), wait for it to run and return its id. An allocation that ends before
|
|
715
|
+
running, or in a failed state (node failure, ...), stops the pool. check: ask Slurm for
|
|
716
|
+
its state even if it was checked in the last 30 s."""
|
|
688
717
|
async with self.alloc_lock:
|
|
689
718
|
if self.alloc_refused:
|
|
690
719
|
raise AllocationRefused(self.alloc_refused)
|
|
691
720
|
if check:
|
|
692
721
|
self.alloc_checked = 0
|
|
693
|
-
n_requested = 0
|
|
694
722
|
while True:
|
|
695
723
|
if self.alloc_id and time.monotonic() - self.alloc_checked < 30:
|
|
696
724
|
return self.alloc_id
|
|
697
725
|
if self.alloc_id is None:
|
|
698
|
-
if n_requested == ALLOC_MAX_REQUESTS:
|
|
699
|
-
raise RuntimeError(f'{n_requested} allocations ended before running')
|
|
700
726
|
self.alloc_id = await self._submit_allocation()
|
|
701
|
-
|
|
727
|
+
self.alloc_ran = False
|
|
702
728
|
_, state, _ = await self._run('squeue', '-h', '-j', self.alloc_id, '-o', '%T')
|
|
703
729
|
if state == 'RUNNING':
|
|
704
730
|
self.alloc_checked = time.monotonic()
|
|
731
|
+
self.alloc_ran = True
|
|
705
732
|
await self._set_cpus(self.alloc_id)
|
|
706
733
|
return self.alloc_id
|
|
707
|
-
if state
|
|
708
|
-
|
|
709
|
-
f'({state or "gone"}): requesting a new one')
|
|
710
|
-
self.alloc_id = None
|
|
734
|
+
if state in ('PENDING', 'CONFIGURING'):
|
|
735
|
+
await asyncio.sleep(5)
|
|
711
736
|
continue
|
|
712
|
-
await
|
|
737
|
+
final = await self._final_state(self.alloc_id) or state or 'gone'
|
|
738
|
+
if final.split()[0] in ALLOC_FAILED_STATES or not self.alloc_ran:
|
|
739
|
+
when = '' if self.alloc_ran else ' before running'
|
|
740
|
+
self.alloc_refused = (f'allocation {self.alloc_id} ended{when} ({final}): no '
|
|
741
|
+
f'new allocation is requested; check the nodes and the '
|
|
742
|
+
f'partition (sacct -j {self.alloc_id})')
|
|
743
|
+
print(style(f'[{self.job_name}] {self.alloc_refused}', fg='red'))
|
|
744
|
+
raise AllocationRefused(self.alloc_refused)
|
|
745
|
+
print(f'[{self.job_name}] allocation {self.alloc_id} ended ({final}): '
|
|
746
|
+
'requesting a new one')
|
|
747
|
+
self.alloc_id = None
|
|
748
|
+
|
|
749
|
+
async def _final_state(self, job_id):
|
|
750
|
+
"""State of an ended job from sacct, e.g. NODE_FAIL or COMPLETED; '' if unknown."""
|
|
751
|
+
returncode, out, _ = await self._run('sacct', '-n', '-X', '-P', '-j', job_id, '-o',
|
|
752
|
+
'State')
|
|
753
|
+
return out.splitlines()[0].strip() if returncode == 0 and out.strip() else ''
|
|
713
754
|
|
|
714
755
|
async def _submit_allocation(self):
|
|
715
756
|
# The holding job: ends after ALLOC_IDLE_MINUTES without step (other than its own)
|
|
@@ -722,7 +763,8 @@ class SlurmClient:
|
|
|
722
763
|
for o in self.options):
|
|
723
764
|
time_option = ['--time=24:00:00']
|
|
724
765
|
# --no-kill: a node that fails ends only the steps on it, not the allocation
|
|
725
|
-
args = ['sbatch', '--parsable', f'--nodes={self.alloc_nodes}', '--exclusive',
|
|
766
|
+
args = ['sbatch', '--parsable', f'--nodes={nodes_option(self.alloc_nodes)}', '--exclusive',
|
|
767
|
+
'--no-kill',
|
|
726
768
|
f'--job-name={self.job_name}:alloc', '--output=/dev/null', *time_option,
|
|
727
769
|
*self.options, '--wrap', holder]
|
|
728
770
|
returncode, out, err = await self._run(*args)
|
|
@@ -734,8 +776,8 @@ class SlurmClient:
|
|
|
734
776
|
raise AllocationRefused(self.alloc_refused)
|
|
735
777
|
raise RuntimeError(f'Requesting the allocation failed: {err}')
|
|
736
778
|
job_id = out.split(';')[0]
|
|
737
|
-
print(f'[{self.job_name}] allocation {job_id} of {self.alloc_nodes}
|
|
738
|
-
'run as job steps')
|
|
779
|
+
print(f'[{self.job_name}] allocation {job_id} of {nodes_option(self.alloc_nodes)} '
|
|
780
|
+
'node(s): the tasks run as job steps')
|
|
739
781
|
return job_id
|
|
740
782
|
|
|
741
783
|
async def _log_stdout(self, task, stream):
|
|
@@ -1016,8 +1058,9 @@ class WorkerPool:
|
|
|
1016
1058
|
'-p cpu -c 16 --mem=64G' (default: $LIBPIPE_SLURM_OPTIONS).
|
|
1017
1059
|
kill_grace_s (int, optional): Time a task past max_time gets to stop after SIGTERM
|
|
1018
1060
|
before SIGKILL.
|
|
1019
|
-
slurm_nodes (int, optional): For 'slurm' from a login node: run the tasks as
|
|
1020
|
-
steps of an allocation of up to that many nodes, instead of one job each
|
|
1061
|
+
slurm_nodes (int or str, optional): For 'slurm' from a login node: run the tasks as
|
|
1062
|
+
job steps of an allocation of up to that many nodes, instead of one job each;
|
|
1063
|
+
'MIN-MAX' starts it once MIN nodes are free, with up to MAX of them.
|
|
1021
1064
|
resources (dict, optional): CPUs and memory of the task categories as job steps,
|
|
1022
1065
|
{category: {'cpus': .., 'mem': ..}}, updating DEFAULT_RESOURCES.
|
|
1023
1066
|
"""
|
|
@@ -1044,7 +1087,7 @@ class WorkerPool:
|
|
|
1044
1087
|
self._waiters = collections.deque()
|
|
1045
1088
|
self._cooldown_timer = None
|
|
1046
1089
|
self.resources = resolve_resources(resources)
|
|
1047
|
-
self.slurm_nodes = slurm_nodes
|
|
1090
|
+
self.slurm_nodes = node_range(slurm_nodes)
|
|
1048
1091
|
# Wall time, CPU time and peak memory of each task (libpipe.taskrun), with the Python
|
|
1049
1092
|
# of the pool: on this machine or on the nodes of the cluster
|
|
1050
1093
|
self.accounting = all(h in (localhost_shortname, 'localhost', SLURM_HOST) for h in hosts)
|
|
@@ -1055,9 +1098,9 @@ class WorkerPool:
|
|
|
1055
1098
|
client = LocalClient(localhost_shortname, force_sync=force_sync)
|
|
1056
1099
|
elif host == SLURM_HOST:
|
|
1057
1100
|
client = SlurmClient(slurm_options, job_name=name, max_time=max_time,
|
|
1058
|
-
alloc_nodes=slurm_nodes)
|
|
1059
|
-
if slurm_nodes:
|
|
1060
|
-
n_workers = max(n_workers, ALLOC_WORKERS_PER_NODE * slurm_nodes)
|
|
1101
|
+
alloc_nodes=self.slurm_nodes)
|
|
1102
|
+
if self.slurm_nodes:
|
|
1103
|
+
n_workers = max(n_workers, ALLOC_WORKERS_PER_NODE * self.slurm_nodes[1])
|
|
1061
1104
|
else:
|
|
1062
1105
|
client = Client(host, user, force_sync=force_sync, password=password,
|
|
1063
1106
|
connect_timeout=connect_timeout)
|
|
@@ -1276,7 +1319,8 @@ class WorkerPool:
|
|
|
1276
1319
|
# An allocation no larger than the number of tasks
|
|
1277
1320
|
for client in {w.client for w in self.workers}:
|
|
1278
1321
|
if isinstance(client, SlurmClient) and client.alloc_nodes:
|
|
1279
|
-
|
|
1322
|
+
hi = min(self.slurm_nodes[1], len(self.tasks))
|
|
1323
|
+
client.alloc_nodes = (min(self.slurm_nodes[0], hi), hi) if hi else None
|
|
1280
1324
|
|
|
1281
1325
|
self.futures = [asyncio.ensure_future(task.run(self)) for task in self.tasks]
|
|
1282
1326
|
self.futures.append(asyncio.ensure_future(self._monitor_queue()))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|