libpipe 0.9.1__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,17 +1,18 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: libpipe
3
- Version: 0.9.1
3
+ Version: 0.10
4
4
  Summary: Pipeline orchestration and node management utilities
5
- License: GPL-3.0-or-later
5
+ License-Expression: GPL-3.0-or-later
6
+ License-File: LICENSE
6
7
  Author: Florent Mertens
7
8
  Author-email: florent.mertens@gmail.com
8
9
  Requires-Python: >=3.10,<4.0
9
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  Provides-Extra: dev
16
17
  Provides-Extra: docs
17
18
  Requires-Dist: asyncssh (>=2.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
- 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):
@@ -700,22 +711,35 @@ class SlurmClient:
700
711
  task.slurm_job_id = None
701
712
  if self.alloc_nodes:
702
713
  await self.ensure_allocation()
703
- 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)
704
719
  try:
705
720
  process = await asyncio.create_subprocess_shell(
706
721
  self.srun_command(task), stdout=asyncio.subprocess.PIPE,
707
722
  stderr=asyncio.subprocess.PIPE, preexec_fn=os.setpgrp, env=self.env)
723
+ task.log_event(f'srun started{self._cpus_state()}')
708
724
  task.set_process(Process(self, process))
709
725
  # srun waits for the job or the step to start: running from the marker line
710
726
  task.start_time = None
711
727
 
712
728
  await asyncio.gather(
713
729
  self._log_stdout(task, process.stdout),
714
- task.log_stream(process.stderr, self.host, err=True),
730
+ task.log_stream(process.stderr, self.host, err=True, stamp_prefix=b'srun: '),
715
731
  )
716
732
  task.returncode = await process.wait()
733
+ task.log_event(f'srun exited with code {task.returncode}')
717
734
  finally:
718
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)'
719
743
 
720
744
  async def terminate(self, task, sig=signal.SIGTERM):
721
745
  job_id = getattr(task, 'slurm_job_id', None)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "libpipe"
7
- version = "0.9.1"
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