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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libpipe
3
- Version: 0.9
3
+ Version: 0.10
4
4
  Summary: Pipeline orchestration and node management utilities
5
5
  License-Expression: GPL-3.0-or-later
6
6
  License-File: LICENSE
@@ -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
- async def log_stream(self, stream, host, err=False):
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
- # --quiet: no "step creation disabled, retrying" while a step waits for its node
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 = await self._acquire_cpus(task.cpus or 1) if self.steps else 0
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}:
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "libpipe"
7
- version = "0.9"
7
+ version = "0.10"
8
8
  description = "Pipeline orchestration and node management utilities"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
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