libpipe 0.9__tar.gz → 0.10__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.9 → libpipe-0.10}/PKG-INFO +1 -1
- {libpipe-0.9 → libpipe-0.10}/libpipe/worker.py +44 -6
- {libpipe-0.9 → libpipe-0.10}/pyproject.toml +1 -1
- {libpipe-0.9 → libpipe-0.10}/LICENSE +0 -0
- {libpipe-0.9 → libpipe-0.10}/README.md +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/__init__.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/attrmap.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/futils.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/msutils.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/settings.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/taskrun.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/tools/__init__.py +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/tools/libnodes_bash.sh +0 -0
- {libpipe-0.9 → libpipe-0.10}/libpipe/tools/nodetool.py +0 -0
|
@@ -51,6 +51,10 @@ ALLOC_MAX_REQUESTS = 3
|
|
|
51
51
|
ALLOC_WORKERS_PER_NODE = 32
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def now_str():
|
|
55
|
+
return datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')
|
|
56
|
+
|
|
57
|
+
|
|
54
58
|
def mem_mb(text):
|
|
55
59
|
'''Memory such as 40GB, 40G or 512MB in MB (GB is 1024 MB, as in Slurm).'''
|
|
56
60
|
m = re.fullmatch(r'\s*(\d+(?:\.\d+)?)\s*([KMGT]?)(?:i?B)?\s*', str(text), re.IGNORECASE)
|
|
@@ -259,7 +263,13 @@ class Task:
|
|
|
259
263
|
line = style(line, fg='red')
|
|
260
264
|
print(f'[{self.name}:{host}] {line}')
|
|
261
265
|
|
|
262
|
-
|
|
266
|
+
def log_event(self, text):
|
|
267
|
+
"""Write a timestamped libpipe event to the log file only."""
|
|
268
|
+
if self.fd is not None:
|
|
269
|
+
self.fd.write(f'[libpipe] {now_str()} {text}\n')
|
|
270
|
+
|
|
271
|
+
async def log_stream(self, stream, host, err=False, stamp_prefix=None):
|
|
272
|
+
"""Log the lines of stream; those starting with stamp_prefix get the current time."""
|
|
263
273
|
while True:
|
|
264
274
|
try:
|
|
265
275
|
line = await stream.readline()
|
|
@@ -268,6 +278,8 @@ class Task:
|
|
|
268
278
|
continue
|
|
269
279
|
if not line:
|
|
270
280
|
break
|
|
281
|
+
if stamp_prefix and line.startswith(stamp_prefix):
|
|
282
|
+
line = f'{now_str()} '.encode() + line
|
|
271
283
|
self.log(line, host, err=err)
|
|
272
284
|
|
|
273
285
|
def close_log(self):
|
|
@@ -618,15 +630,14 @@ class SlurmClient:
|
|
|
618
630
|
if self.alloc_id:
|
|
619
631
|
args.append(f'--jobid={self.alloc_id}')
|
|
620
632
|
if self.exact:
|
|
621
|
-
|
|
622
|
-
args += ['--exact', '--quiet']
|
|
633
|
+
args.append('--exact')
|
|
623
634
|
if task.cpus:
|
|
624
635
|
args += [f'--cpus-per-task={task.cpus}', f'--mem={task.mem_mb}M']
|
|
625
636
|
threads = ' '.join(f'{v}={task.cpus}' for v in THREAD_VARS)
|
|
626
637
|
command = f'export {threads}; {command}'
|
|
627
638
|
job_id, options = '${SLURM_JOB_ID}.${SLURM_STEP_ID}', []
|
|
628
639
|
args += self._time_option(options) + options
|
|
629
|
-
marker = f'echo "{SLURM_MARKER}{job_id} on $(hostname -s)"'
|
|
640
|
+
marker = f'echo "{SLURM_MARKER}{job_id} on $(hostname -s) at $(date \'+%F %T.%3N\')"'
|
|
630
641
|
return f'{shlex.join(args)} bash -c {shlex.quote(f"{marker}; {command}")}'
|
|
631
642
|
|
|
632
643
|
async def _run(self, *args):
|
|
@@ -693,26 +704,42 @@ class SlurmClient:
|
|
|
693
704
|
text = line.decode(errors='replace')
|
|
694
705
|
if task.slurm_job_id is None and text.startswith(SLURM_MARKER):
|
|
695
706
|
task.slurm_job_id = text[len(SLURM_MARKER):].split()[0]
|
|
707
|
+
task.start_time = time.time()
|
|
696
708
|
task.log(line, self.host)
|
|
697
709
|
|
|
698
710
|
async def execute(self, task):
|
|
699
711
|
task.slurm_job_id = None
|
|
700
712
|
if self.alloc_nodes:
|
|
701
713
|
await self.ensure_allocation()
|
|
702
|
-
cpus =
|
|
714
|
+
cpus = 0
|
|
715
|
+
if self.steps and self.cpus_total is not None:
|
|
716
|
+
task.log_event(f'waiting for {min(task.cpus or 1, self.cpus_total)} CPUs '
|
|
717
|
+
f'({self.cpus_free} of {self.cpus_total} free)')
|
|
718
|
+
cpus = await self._acquire_cpus(task.cpus or 1)
|
|
703
719
|
try:
|
|
704
720
|
process = await asyncio.create_subprocess_shell(
|
|
705
721
|
self.srun_command(task), stdout=asyncio.subprocess.PIPE,
|
|
706
722
|
stderr=asyncio.subprocess.PIPE, preexec_fn=os.setpgrp, env=self.env)
|
|
723
|
+
task.log_event(f'srun started{self._cpus_state()}')
|
|
707
724
|
task.set_process(Process(self, process))
|
|
725
|
+
# srun waits for the job or the step to start: running from the marker line
|
|
726
|
+
task.start_time = None
|
|
708
727
|
|
|
709
728
|
await asyncio.gather(
|
|
710
729
|
self._log_stdout(task, process.stdout),
|
|
711
|
-
task.log_stream(process.stderr, self.host, err=True),
|
|
730
|
+
task.log_stream(process.stderr, self.host, err=True, stamp_prefix=b'srun: '),
|
|
712
731
|
)
|
|
713
732
|
task.returncode = await process.wait()
|
|
733
|
+
task.log_event(f'srun exited with code {task.returncode}')
|
|
714
734
|
finally:
|
|
715
735
|
await self._release_cpus(cpus)
|
|
736
|
+
if cpus:
|
|
737
|
+
task.log_event(f'{cpus} CPUs released{self._cpus_state()}')
|
|
738
|
+
|
|
739
|
+
def _cpus_state(self):
|
|
740
|
+
if self.cpus_total is None:
|
|
741
|
+
return ''
|
|
742
|
+
return f' ({self.cpus_free} of {self.cpus_total} CPUs free)'
|
|
716
743
|
|
|
717
744
|
async def terminate(self, task, sig=signal.SIGTERM):
|
|
718
745
|
job_id = getattr(task, 'slurm_job_id', None)
|
|
@@ -1096,6 +1123,7 @@ class WorkerPool:
|
|
|
1096
1123
|
tasks = self.get_all_tasks()
|
|
1097
1124
|
n_done = len(tasks[TaskStatus.SUCCESS])
|
|
1098
1125
|
n_todo = len(tasks[TaskStatus.WAITING]) + len(tasks[TaskStatus.RUNNING])
|
|
1126
|
+
self._start_pbar_timer()
|
|
1099
1127
|
self.pbar.update(n_done)
|
|
1100
1128
|
|
|
1101
1129
|
if self.max_time is not None:
|
|
@@ -1104,6 +1132,15 @@ class WorkerPool:
|
|
|
1104
1132
|
except asyncio.CancelledError:
|
|
1105
1133
|
pass
|
|
1106
1134
|
|
|
1135
|
+
def _start_pbar_timer(self):
|
|
1136
|
+
"""Time the pool from the first task start, without the wait for the Slurm jobs."""
|
|
1137
|
+
if self.pbar_timed:
|
|
1138
|
+
return
|
|
1139
|
+
starts = [t.start_time for t in self.tasks if t.start_time is not None]
|
|
1140
|
+
if starts:
|
|
1141
|
+
self.pbar.start_time = datetime.datetime.fromtimestamp(min(starts))
|
|
1142
|
+
self.pbar_timed = True
|
|
1143
|
+
|
|
1107
1144
|
async def _enforce_max_time(self):
|
|
1108
1145
|
now = time.time()
|
|
1109
1146
|
for worker in self.workers:
|
|
@@ -1150,6 +1187,7 @@ class WorkerPool:
|
|
|
1150
1187
|
self.tasks.sort(key=lambda task: not task.run_on_host)
|
|
1151
1188
|
|
|
1152
1189
|
self.pbar.start(max_value=len(self.tasks))
|
|
1190
|
+
self.pbar_timed = False
|
|
1153
1191
|
|
|
1154
1192
|
# An allocation no larger than the number of tasks
|
|
1155
1193
|
for client in {w.client for w in self.workers}:
|
|
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
|
|
File without changes
|