diff --git a/dask_jobqueue/lsf.py b/dask_jobqueue/lsf.py index d84afbf7..d079ac55 100644 --- a/dask_jobqueue/lsf.py +++ b/dask_jobqueue/lsf.py @@ -87,7 +87,7 @@ def __init__( memory_string = lsf_format_bytes_ceil(mem, lsf_units=lsf_units) header_lines.append("#BSUB -M %s" % memory_string) if walltime is not None: - header_lines.append("#BSUB -W %s" % walltime) + header_lines.append("#BSUB -W %s" % lsf_format_walltime(walltime)) # Skip requested header directives header_lines = list( @@ -136,6 +136,37 @@ def lsf_format_bytes_ceil(n, lsf_units="mb"): return "%d" % math.ceil(n / (1000 ** converter[lsf_units])) +def lsf_format_walltime(walltime): + """Format a walltime as ``[hour:]minute``, which is what LSF requires + + The ``HH:MM:SS`` format accepted by the other job schedulers is converted, + rounding any leftover seconds up to the next minute so that the job is + never given less time than requested. Any other value is returned + unchanged. + + Parameters + ---------- + walltime: str + Walltime of the job + + Examples + -------- + >>> lsf_format_walltime("01:30:00") + '01:30' + >>> lsf_format_walltime("00:00:30") + '00:01' + >>> lsf_format_walltime("01:30") + '01:30' + """ + parts = str(walltime).split(":") + if len(parts) != 3 or not all(part.isdigit() for part in parts): + return walltime + hours, minutes, seconds = (int(part) for part in parts) + total_minutes = hours * 60 + minutes + math.ceil(seconds / 60) + hours, minutes = divmod(total_minutes, 60) + return "%02d:%02d" % (hours, minutes) + + def lsf_detect_units(): """Try to autodetect the unit scaling on an LSF system""" # Search for automatically, Using docs from LSF 9.1.3 for search/defaults @@ -188,7 +219,8 @@ class LSFCluster(JobQueueCluster): mem : int Request memory in bytes. Passed to `#BSUB -M` option. walltime : str - Walltime for each worker job in HH:MM. Passed to `#BSUB -W` option. + Walltime for each worker job in HH:MM. A walltime given as HH:MM:SS is + converted to HH:MM, rounding up. Passed to `#BSUB -W` option. {cluster} job_extra : list Deprecated: use ``job_extra_directives`` instead. This parameter will be removed in a future version. diff --git a/dask_jobqueue/tests/test_lsf.py b/dask_jobqueue/tests/test_lsf.py index 5f8c7435..9680d81f 100644 --- a/dask_jobqueue/tests/test_lsf.py +++ b/dask_jobqueue/tests/test_lsf.py @@ -79,6 +79,27 @@ def test_header(): assert "#BSUB -P" not in cluster.job_header +@pytest.mark.parametrize( + "walltime, expected", + [ + ("01:00:00", "01:00"), + ("00:30:00", "00:30"), + ("2:15:00", "02:15"), + ("00:00:30", "00:01"), + ("01:59:01", "02:00"), + ("48:00:00", "48:00"), + ("01:00", "01:00"), + ("1:00", "1:00"), + ("90", "90"), + ], +) +def test_walltime_format(walltime, expected): + assert lsf.lsf_format_walltime(walltime) == expected + + with LSFCluster(walltime=walltime, cores=1, memory="1GB") as cluster: + assert "#BSUB -W %s" % expected in cluster.job_header.splitlines() + + def test_job_script(): with LSFCluster(walltime="00:02", processes=4, cores=8, memory="28GB") as cluster: job_script = cluster.job_script() diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b6661ffa..b38dc0f6 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,8 @@ Changelog Development version ------------------- +- Fixed ``LSFCluster`` generating an invalid ``#BSUB -W`` directive for a walltime in ``HH:MM:SS`` format (:issue:`703`) + 0.9.0 / 2024-08-22 ------------------