Skip to content

Commit 07362e5

Browse files
lguerardclaude
andcommitted
Merge branch dev: SLURM job naming and docs anchor fixes
Gives every SLURM job a recognisable name in the queue via the executor plugin jobname prefix, set per config by run_multi so a three-way run shows pw-convert then pw-nuclei_labels / pw-cyto_labels / pw-cilia_labels rather than three indistinguishable UUIDs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 parents 8cef606 + 40b0514 commit 07362e5

6 files changed

Lines changed: 101 additions & 3 deletions

File tree

docs/guide/label_relations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ table[2]
2424
`table` only contains matched `a` labels (nuclei with at least one
2525
overlapping voxel in `cells`) — unmatched labels and full per-`b` coverage
2626
need a bit more bookkeeping (the [cluster workflow's `run_multi.py`
27-
script](snakemake.md#one-command-multiple-segmentations--relations) does
27+
script](snakemake.md#one-command-multiple-segmentations-relations) does
2828
this for you and writes it as a two-sheet workbook).
2929

3030
Save it as a table yourself:
@@ -41,4 +41,4 @@ with open("nuclei_to_cell.csv", "w", newline="") as f:
4141

4242
On the cluster, producing the two label stores in the first place is a
4343
matter of running the workflow twice against the same `work_dir` — see
44-
[Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei--cytoplasm).
44+
[Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei-cytoplasm).

docs/guide/measurements.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ is built for this — its "Measure" dock widget computes area/centroid/intensity
1010
stats directly off a Labels layer's dask/zarr-backed array, out-of-core, and
1111
scales with chunk count rather than object count. It's the best fit for
1212
measuring *every* object in a store this size, not just a cropped region —
13-
see [View image + labels in napari](ome_zarr_napari.md#view-image--labels-in-napari).
13+
see [View image + labels in napari](ome_zarr_napari.md#view-image-labels-in-napari).
1414
Bundled in `patchworks[napari]`.
1515

1616
For interactively inspecting individual cells by clicking in the viewer (not

docs/guide/snakemake.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ Snakemake submits `convert`, then `prepare`, then **one `segment` job per
187187
batch of `tiles_per_job` non-empty tiles** (up to `jobs:` at once → that many
188188
GPUs in parallel), then `merge`. Raise `jobs:` to use more GPUs.
189189

190+
!!! tip "Recognisable job names in `squeue`"
191+
The SLURM executor names every job after its run UUID and **rejects** a
192+
`--job-name` in `slurm_extra`, so by default `squeue` shows nothing you
193+
can identify. A prefix is the supported lever, and it goes first in the
194+
name (`<prefix>_<uuid>`) — the part a queue listing truncates to:
195+
196+
```yaml
197+
slurm-jobname-prefix: patchworks # already in the shipped profile
198+
```
199+
200+
`run_multi` overrides it per config, so a three-way run shows
201+
`pw-convert`, then `pw-nuclei_labels` / `pw-cyto_labels` /
202+
`pw-cilia_labels` — telling the concurrent runs apart at a glance:
203+
204+
```bash
205+
squeue -u $USER -o '%.18i %.24j %.8T %.10M'
206+
```
207+
208+
Alphanumerics, underscores and hyphens only, 50 characters max; an
209+
invalid prefix fails the run, so `label_name` is sanitised before use.
210+
190211
!!! tip "Sizing memory"
191212
Every step now sizes its own worker counts from what SLURM actually
192213
granted (`SLURM_CPUS_PER_TASK`, `SLURM_MEM_PER_*`, the cgroup limit)

tests/test_run_multi.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Tests for the multi-config driver's SLURM-facing behaviour."""
2+
3+
import re
4+
import sys
5+
from pathlib import Path
6+
7+
sys.path.insert(
8+
0, str(Path(__file__).resolve().parents[1] / "workflow" / "scripts")
9+
)
10+
11+
from run_multi import slurm_jobname_prefix # noqa: E402
12+
13+
# The SLURM executor's own rule (snakemake_executor_plugin_slurm): it raises a
14+
# WorkflowError and aborts the whole run if the prefix does not match.
15+
_EXECUTOR_RULE = re.compile(r"^[A-Za-z0-9_-]{1,50}$")
16+
17+
18+
def test_jobname_prefix_satisfies_the_executor():
19+
"""Whatever a label_name contains, the prefix must stay submittable.
20+
21+
The executor names jobs after a UUID and refuses a --job-name override, so
22+
this prefix is the only thing that makes squeue readable -- and an invalid
23+
one fails the run rather than degrading.
24+
"""
25+
for label in ("nuclei_labels", "cyto_labels", "convert", "a"):
26+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix(label))
27+
28+
# Characters a label might plausibly pick up are sanitised, not passed on.
29+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("cilia/v2 (test)"))
30+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("run 1: nuclei"))
31+
# And an over-long label is truncated to the executor's 50-char limit.
32+
assert _EXECUTOR_RULE.match(slurm_jobname_prefix("x" * 200))
33+
34+
35+
def test_jobname_prefix_keeps_the_label_readable():
36+
"""The label must lead, since that is what a queue listing truncates to."""
37+
assert slurm_jobname_prefix("nuclei_labels") == "pw-nuclei_labels"
38+
assert slurm_jobname_prefix("convert") == "pw-convert"

workflow/profile/slurm/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
# Edit the partitions / account / GPU request for your cluster.
66

77
executor: slurm
8+
# The SLURM executor names every job after its run UUID and rejects a
9+
# --job-name in slurm_extra, so `squeue` shows nothing recognisable without
10+
# this. The prefix goes first in the name (<prefix>_<uuid>), which is the part
11+
# that survives truncation in a queue listing.
12+
# Alphanumerics, underscores and hyphens only; 50 characters max.
13+
# run_multi overrides this per config, so a three-way run shows pw-convert,
14+
# then pw-nuclei_labels / pw-cyto_labels / pw-cilia_labels.
15+
slurm-jobname-prefix: patchworks
816
# Max concurrent SLURM jobs Snakemake will have in flight at once. This is a
917
# submission-side cap, not a cluster limit — raising it just lets more
1018
# "segment" tiles queue/run together; scicore's scheduler still enforces your

workflow/scripts/run_multi.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from __future__ import annotations
2727

2828
import argparse
29+
import re
2930
import subprocess
3031
import sys
3132
from pathlib import Path
@@ -47,6 +48,7 @@ def _snakemake_cmd(
4748
state_dir: Path | None = None,
4849
targets: list[str] | None = None,
4950
extra: list[str] | None = None,
51+
jobname_prefix: str | None = None,
5052
) -> list[str]:
5153
"""Build one snakemake invocation.
5254
@@ -66,6 +68,9 @@ def _snakemake_cmd(
6668
cmd += ["--directory", str(state_dir.resolve())]
6769
if profile:
6870
cmd += ["--workflow-profile", str((workflow_dir / profile).resolve())]
71+
if jobname_prefix:
72+
# A SLURM-executor setting, so only valid alongside the profile.
73+
cmd += ["--slurm-jobname-prefix", jobname_prefix]
6974
else:
7075
cmd += ["--cores", str(cores), "--rerun-triggers", "mtime"]
7176
if dry_run:
@@ -79,6 +84,29 @@ def _snakemake_cmd(
7984
return cmd
8085

8186

87+
def slurm_jobname_prefix(label: str) -> str:
88+
"""Sanitise *label* into a SLURM job-name prefix the executor accepts.
89+
90+
The SLURM executor names every job after its run UUID and refuses a
91+
``--job-name`` in ``slurm_extra``, so a prefix is the only way to get
92+
something recognisable into ``squeue``. It becomes ``<prefix>_<uuid>``,
93+
which puts the readable part first -- the part that survives truncation
94+
in a queue listing.
95+
96+
The executor requires alphanumerics, underscores and hyphens only, at
97+
most 50 characters, and rejects the whole run otherwise.
98+
99+
Examples
100+
--------
101+
>>> slurm_jobname_prefix("nuclei_labels")
102+
'pw-nuclei_labels'
103+
>>> slurm_jobname_prefix("cilia/v2 (test)")
104+
'pw-cilia-v2--test-'
105+
"""
106+
safe = re.sub(r"[^A-Za-z0-9_-]", "-", label)
107+
return f"pw-{safe}"[:50]
108+
109+
82110
def _run(cmd: list[str], workflow_dir: Path) -> int:
83111
print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
84112
return subprocess.run(cmd, cwd=workflow_dir).returncode
@@ -235,6 +263,7 @@ def main() -> None:
235263
dry_run=args.dry_run,
236264
state_dir=Path(work_dir) / ".snakemake_convert",
237265
targets=[f"{image_store}/zarr.json"],
266+
jobname_prefix=slurm_jobname_prefix("convert"),
238267
),
239268
workflow_dir,
240269
)
@@ -287,6 +316,8 @@ def main() -> None:
287316
cores=args.cores,
288317
dry_run=args.dry_run,
289318
state_dir=Path(cfg["work_dir"]) / cfg["label_name"] / ".snakemake",
319+
# Names the config in squeue, so concurrent runs are tellable apart.
320+
jobname_prefix=slurm_jobname_prefix(cfg["label_name"]),
290321
)
291322
print(f"[run_multi] $ {' '.join(cmd)}", flush=True)
292323
procs.append((cfg_path.name, subprocess.Popen(cmd, cwd=workflow_dir)))

0 commit comments

Comments
 (0)