patchworks 2.6.0__tar.gz → 2.6.2__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.
- {patchworks-2.6.0 → patchworks-2.6.2}/PKG-INFO +1 -1
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/gpu_distributed.md +5 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/snakemake.md +33 -3
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/tiling.md +17 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_chunks.py +55 -9
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_allocation.py +42 -1
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_core.py +37 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_run_multi.py +27 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/README.md +5 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/config_cyto.yaml +5 -3
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/profile/slurm/config.yaml +9 -1
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/prepare_tiles.py +9 -1
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/run_multi.py +18 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/.github/workflows/docs.yml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/.github/workflows/lint.yml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/.github/workflows/release.yml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/.gitignore +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/LICENSE +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/README.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/cliff.toml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/chunks.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/cluster.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/io.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/postprocess.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/relabel.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/api/tile_process.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/assets/logo.png +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/custom.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/custom_method.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/dog.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/dog.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/stardist.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/getting_started.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/label_relations.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/measurements.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/merging.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/performance.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/docs/index.md +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/mkdocs.yml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/pyproject.toml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/__init__.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_core.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_io.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_merge.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_notify.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_progress.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/_relations.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_distributed.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_dog.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_gpu.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_napari.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_notify.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_occupancy.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_postprocess.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_progress.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/tests/test_relations.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/Snakefile +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/common.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/config.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/config/multi.yaml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/pixi.toml +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/rules/common.smk +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/rules/convert.smk +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/rules/merge.smk +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/rules/segment.smk +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/build_occupancy.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/convert.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/merge.py +0 -0
- {patchworks-2.6.0 → patchworks-2.6.2}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: Tiled processing of arbitrarily large images with globally consistent labels
|
|
5
5
|
Project-URL: Homepage, https://github.com/imcf/patchworks
|
|
6
6
|
Project-URL: Issues, https://github.com/imcf/patchworks/issues
|
|
@@ -79,6 +79,11 @@ own outright. `auto_tile_shape` then sizes each tile to at most half of that
|
|
|
79
79
|
budget; `auto_tile_shape_cellpose` uses Cellpose's own memory model instead
|
|
80
80
|
(roughly 20× the raw tile bytes, plus ~2 GiB for the model).
|
|
81
81
|
|
|
82
|
+
"Raw tile bytes" counts every channel a tile carries, so pass `n_channels=2`
|
|
83
|
+
when feeding Cellpose a cyto+nuclei pair — see
|
|
84
|
+
[Multi-channel tiles](tiling.md#multi-channel-tiles). The Snakemake workflow
|
|
85
|
+
does this for you whenever `nuclei_channel` is set.
|
|
86
|
+
|
|
82
87
|
The device is resolved from `CUDA_VISIBLE_DEVICES`. This matters on
|
|
83
88
|
multi-GPU nodes: NVML enumerates **every** GPU regardless of `--gres=gpu:1`,
|
|
84
89
|
so querying index 0 unconditionally would read a different card's free memory
|
|
@@ -109,6 +109,15 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
109
109
|
explicit z (like `[16, 1024, 1024]`) tiles in z instead. `prepare` logs
|
|
110
110
|
which regime it picked.
|
|
111
111
|
|
|
112
|
+
`"auto"` also caps the tile to the **host** RAM available to the job, not
|
|
113
|
+
just VRAM — a `do_3D` tile that comfortably fits a big GPU can still be
|
|
114
|
+
too large for the SLURM/cgroup memory the job was actually granted, and
|
|
115
|
+
that shows up as a plain `SIGKILL`, not a catchable CUDA-OOM error.
|
|
116
|
+
Because `prepare` runs on a CPU node, it checks *its own* grant as a
|
|
117
|
+
stand-in for `segment`'s — keep `prepare`'s and `segment`'s `mem_mb` in
|
|
118
|
+
`profile/slurm/config.yaml` equal, or the estimate is sized against the
|
|
119
|
+
wrong job's budget.
|
|
120
|
+
|
|
112
121
|
!!! tip "Use a per-axis `overlap`"
|
|
113
122
|
A scalar halo is applied to every axis. On a `[16, 1024, 1024]` tile,
|
|
114
123
|
`overlap: 30` reads `76 × 1084 × 1084` to keep `16 × 1024 × 1024` — 5.3×
|
|
@@ -321,14 +330,35 @@ occupancy map and the staged labels are byte-for-byte what a single-channel
|
|
|
321
330
|
run produces, and `merge` and `label_relations` need no changes. Two things
|
|
322
331
|
follow from that:
|
|
323
332
|
|
|
324
|
-
- A tile holds twice the bytes
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
- A tile holds twice the bytes. `tile_shape: "auto"` accounts for this — it is
|
|
334
|
+
told the tile carries two channels and shrinks each spatial side by ~1/√2,
|
|
335
|
+
so the tile still fits the same VRAM *and* host-RAM budget (see the "Tile
|
|
336
|
+
size vs runtime" tip above). A **hand-set** `tile_shape` sized to fill a
|
|
337
|
+
GPU has no such protection and needs halving yourself.
|
|
327
338
|
- The translation is version-specific. Cellpose 3 gets `channels: [1, 2]`
|
|
328
339
|
(1-based into the channel axis, `0` = grayscale); Cellpose 4 (cpsam) dropped
|
|
329
340
|
`channels` entirely and simply reads both. Either is overridable by setting
|
|
330
341
|
`channels:` or `channel_axis:` in the `cellpose:` block.
|
|
331
342
|
|
|
343
|
+
!!! warning "In a `multi.yaml` run, pin `tile_shape` explicitly"
|
|
344
|
+
|
|
345
|
+
`label_relations` requires its two label arrays to share a chunk layout,
|
|
346
|
+
and that layout comes from `tile_shape`. Giving *one* config a
|
|
347
|
+
`nuclei_channel` while the group uses `tile_shape: "auto"` produces a
|
|
348
|
+
**smaller** tile for that config only — so the label groups end up chunked
|
|
349
|
+
differently and the relations step fails, after every segmentation has
|
|
350
|
+
already run.
|
|
351
|
+
|
|
352
|
+
`run_multi`'s cross-config check compares the configured values, and
|
|
353
|
+
`"auto" == "auto"`, so it flags this case specifically. Fix it by setting
|
|
354
|
+
one explicit `tile_shape` in `common.yaml`, sized for the two-channel
|
|
355
|
+
config (roughly each spatial side ÷ √2 versus what you would use for a
|
|
356
|
+
single channel), so every config shares it.
|
|
357
|
+
|
|
358
|
+
If you do not need that segmentation related to the others, the alternative is
|
|
359
|
+
to run it on its own against the same `work_dir` and leave it out of
|
|
360
|
+
`multi.yaml`.
|
|
361
|
+
|
|
332
362
|
`nuclei_channel` applies to the SLURM/Snakemake path. The single-process
|
|
333
363
|
`tile_process` API still takes one `channel`.
|
|
334
364
|
|
|
@@ -57,6 +57,23 @@ tile_process("image.zarr", fn, tile_shape=tile_fn)
|
|
|
57
57
|
The callable is called with `(shape, dtype)` at runtime, after the image is
|
|
58
58
|
loaded — useful when you don't know the image shape in advance.
|
|
59
59
|
|
|
60
|
+
### Multi-channel tiles
|
|
61
|
+
|
|
62
|
+
Both sizers take `n_channels` (default 1) and charge it per voxel. Use it when
|
|
63
|
+
a tile carries more than one channel while its *geometry* stays spatial — the
|
|
64
|
+
case the Snakemake workflow's
|
|
65
|
+
[`nuclei_channel`](snakemake.md#giving-cellpose-a-nuclei-channel) creates, where
|
|
66
|
+
Cellpose is handed a cyto+nuclei pair but still returns one label per voxel:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Same VRAM budget, so each spatial side shrinks by ~1/√2
|
|
70
|
+
auto_tile_shape_cellpose(shape, dtype, diameter=30, use_gpu=True, n_channels=2)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Leaving it at 1 for a 2-channel tile budgets for half the bytes the tile
|
|
74
|
+
actually holds, which surfaces as an out-of-memory error in the first tile
|
|
75
|
+
rather than as anything about channels.
|
|
76
|
+
|
|
60
77
|
## Overlap
|
|
61
78
|
|
|
62
79
|
Methods that need spatial context (Cellpose, StarDist, U-Net) produce wrong
|
|
@@ -264,6 +264,7 @@ def auto_tile_shape(
|
|
|
264
264
|
gpu_memory: int | None = None,
|
|
265
265
|
available_memory: int | None = None,
|
|
266
266
|
n_workers: int | None = None,
|
|
267
|
+
n_channels: int = 1,
|
|
267
268
|
verbose: bool = False,
|
|
268
269
|
) -> tuple[int, ...]:
|
|
269
270
|
"""Balanced tile shape for general-purpose 3-D processing.
|
|
@@ -280,13 +281,21 @@ def auto_tile_shape(
|
|
|
280
281
|
target_bytes:
|
|
281
282
|
Memory ceiling per tile. Default 64 MiB.
|
|
282
283
|
use_gpu:
|
|
283
|
-
Size tiles against GPU VRAM rather than host RAM.
|
|
284
|
+
Size tiles against GPU VRAM rather than host RAM. Still capped by
|
|
285
|
+
host RAM too -- see ``available_memory``.
|
|
284
286
|
gpu_memory:
|
|
285
287
|
Available GPU VRAM in bytes; auto-queried when None.
|
|
286
288
|
available_memory:
|
|
287
|
-
Available host RAM in bytes; auto-queried when None.
|
|
289
|
+
Available host RAM in bytes; auto-queried when None. Consulted even
|
|
290
|
+
when ``use_gpu`` is set: the tile has to be loaded into host memory
|
|
291
|
+
before (and often after) it reaches the GPU, so it must fit both
|
|
292
|
+
budgets, not just VRAM.
|
|
288
293
|
n_workers:
|
|
289
294
|
Number of parallel workers (divides the RAM budget).
|
|
295
|
+
n_channels:
|
|
296
|
+
Channels each tile carries (default 1). Above 1 the per-voxel cost
|
|
297
|
+
scales with it, so the tile shrinks accordingly -- e.g. the workflow's
|
|
298
|
+
``nuclei_channel`` hands Cellpose a cyto+nuclei pair.
|
|
290
299
|
verbose:
|
|
291
300
|
Log the chosen shape and estimated tile size.
|
|
292
301
|
|
|
@@ -302,11 +311,22 @@ def auto_tile_shape(
|
|
|
302
311
|
(128, 512, 512)
|
|
303
312
|
"""
|
|
304
313
|
n_workers = n_workers or cpu_allocation()
|
|
305
|
-
|
|
314
|
+
# A tile holds n_channels planes per voxel (e.g. Cellpose's
|
|
315
|
+
# cyto+nuclei pair), so the per-voxel cost -- and every budget
|
|
316
|
+
# derived from it below -- scales with them.
|
|
317
|
+
if n_channels < 1:
|
|
318
|
+
raise ValueError(f"n_channels must be >= 1; got {n_channels!r}")
|
|
319
|
+
itemsize = np.dtype(dtype).itemsize * n_channels
|
|
306
320
|
n_spatial = min(3, len(shape))
|
|
307
321
|
|
|
308
322
|
if use_gpu:
|
|
309
|
-
|
|
323
|
+
# A tile that fits VRAM still has to be decoded into (and often
|
|
324
|
+
# copied back out of) host RAM first, so a GPU with more memory
|
|
325
|
+
# than the job's own host allocation must not produce a tile the
|
|
326
|
+
# job itself can't hold -- take whichever budget is tighter.
|
|
327
|
+
gpu_mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
|
|
328
|
+
host_mem = available_memory or _get_available_memory()
|
|
329
|
+
mem = min(gpu_mem, host_mem)
|
|
310
330
|
budget = min(target_bytes * 2, mem // 2)
|
|
311
331
|
else:
|
|
312
332
|
mem = available_memory or _get_available_memory()
|
|
@@ -359,6 +379,7 @@ def auto_tile_shape_cellpose(
|
|
|
359
379
|
n_workers: int | None = None,
|
|
360
380
|
model_memory_bytes: int = 2 * 1024**3,
|
|
361
381
|
cellpose_memory_factor: int = 20,
|
|
382
|
+
n_channels: int = 1,
|
|
362
383
|
verbose: bool = False,
|
|
363
384
|
) -> tuple[int, ...]:
|
|
364
385
|
"""Cellpose-optimised tile shape.
|
|
@@ -384,13 +405,27 @@ def auto_tile_shape_cellpose(
|
|
|
384
405
|
do_3D:
|
|
385
406
|
Whether Cellpose will run in 3-D mode.
|
|
386
407
|
use_gpu:
|
|
387
|
-
Size tiles for GPU VRAM.
|
|
408
|
+
Size tiles for GPU VRAM. Still capped by host RAM too -- see
|
|
409
|
+
``available_memory``.
|
|
388
410
|
gpu_memory, available_memory, n_workers:
|
|
389
|
-
Memory parameters (auto-queried when None).
|
|
411
|
+
Memory parameters (auto-queried when None). ``available_memory`` is
|
|
412
|
+
consulted even when ``use_gpu`` is set: Cellpose's ``do_3D``
|
|
413
|
+
preprocessing (orthogonal-view flows, mask assembly) copies the tile
|
|
414
|
+
through host memory, so VRAM headroom alone doesn't guarantee the
|
|
415
|
+
job's own host allocation can hold it.
|
|
390
416
|
model_memory_bytes:
|
|
391
417
|
Memory consumed by the Cellpose model weights (default 2 GiB).
|
|
392
418
|
cellpose_memory_factor:
|
|
393
|
-
Cellpose allocates roughly this multiple of raw input bytes (default
|
|
419
|
+
Cellpose allocates roughly this multiple of raw input bytes (default
|
|
420
|
+
20×). Applied against whichever of VRAM/host RAM is tighter --
|
|
421
|
+
``ponytail:`` this reuses one factor for both budgets as an
|
|
422
|
+
approximation; if it proves off in practice, calibrate a separate
|
|
423
|
+
host-side factor from a SLURM job's peak RSS (``seff``/
|
|
424
|
+
``/usr/bin/time -v``) and pass it alongside this one.
|
|
425
|
+
n_channels:
|
|
426
|
+
Channels each tile carries (default 1). Above 1 the per-voxel cost
|
|
427
|
+
scales with it, so the tile shrinks accordingly -- e.g. the workflow's
|
|
428
|
+
``nuclei_channel`` hands Cellpose a cyto+nuclei pair.
|
|
394
429
|
verbose:
|
|
395
430
|
Log the chosen shape and memory estimates.
|
|
396
431
|
|
|
@@ -406,10 +441,21 @@ def auto_tile_shape_cellpose(
|
|
|
406
441
|
(1, 2048, 2048)
|
|
407
442
|
"""
|
|
408
443
|
n_workers = n_workers or cpu_allocation()
|
|
409
|
-
|
|
444
|
+
# A tile holds n_channels planes per voxel (e.g. Cellpose's
|
|
445
|
+
# cyto+nuclei pair), so the per-voxel cost -- and every budget
|
|
446
|
+
# derived from it below -- scales with them.
|
|
447
|
+
if n_channels < 1:
|
|
448
|
+
raise ValueError(f"n_channels must be >= 1; got {n_channels!r}")
|
|
449
|
+
itemsize = np.dtype(dtype).itemsize * n_channels
|
|
410
450
|
|
|
411
451
|
if use_gpu:
|
|
412
|
-
|
|
452
|
+
# A tile that fits VRAM still has to be decoded into (and often
|
|
453
|
+
# copied back out of) host RAM first, so a GPU with more memory
|
|
454
|
+
# than the job's own host allocation must not produce a tile the
|
|
455
|
+
# job itself can't hold -- take whichever budget is tighter.
|
|
456
|
+
gpu_mem = gpu_memory if gpu_memory is not None else _get_gpu_memory()
|
|
457
|
+
host_mem = available_memory or _get_available_memory()
|
|
458
|
+
total_mem = min(gpu_mem, host_mem)
|
|
413
459
|
else:
|
|
414
460
|
total_mem = (available_memory or _get_available_memory()) // n_workers
|
|
415
461
|
|
|
@@ -8,7 +8,11 @@ checks that would have caught it.
|
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
|
|
11
|
-
from patchworks import
|
|
11
|
+
from patchworks import (
|
|
12
|
+
auto_tile_shape_cellpose,
|
|
13
|
+
cpu_allocation,
|
|
14
|
+
safe_worker_count,
|
|
15
|
+
)
|
|
12
16
|
from patchworks._chunks import _get_available_memory
|
|
13
17
|
|
|
14
18
|
GIB = 1024**3
|
|
@@ -76,3 +80,40 @@ def test_worker_count_is_bounded_by_the_allocation(monkeypatch):
|
|
|
76
80
|
n = safe_worker_count(chunk_nbytes * 40, fn_overhead=3)
|
|
77
81
|
assert n < 32, "must not size itself to the core count when RAM is tighter"
|
|
78
82
|
assert n >= 1
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_gpu_tile_sizing_is_bounded_by_the_host_allocation(monkeypatch):
|
|
86
|
+
"""A big GPU must not excuse a tile the job's own host RAM can't hold.
|
|
87
|
+
|
|
88
|
+
This is the concrete failure a `do_3D` nuclei segmentation hit: an ample
|
|
89
|
+
GPU (24 GiB) sized the tile against VRAM alone, and the job -- granted
|
|
90
|
+
only 1 GiB of host RAM here -- was SIGKILLed loading it, unrelated to
|
|
91
|
+
``nuclei_channel``. The sizer must take whichever budget is tighter.
|
|
92
|
+
"""
|
|
93
|
+
monkeypatch.delenv("SLURM_MEM_PER_CPU", raising=False)
|
|
94
|
+
monkeypatch.setenv("SLURM_MEM_PER_NODE", str(1024)) # 1 GiB
|
|
95
|
+
monkeypatch.setattr(
|
|
96
|
+
"patchworks._chunks._cgroup_memory_limit", lambda: 512 * GIB
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
tile = auto_tile_shape_cellpose(
|
|
100
|
+
(128, 2048, 2048),
|
|
101
|
+
"uint16",
|
|
102
|
+
diameter=30,
|
|
103
|
+
do_3D=True,
|
|
104
|
+
use_gpu=True,
|
|
105
|
+
gpu_memory=24 * GIB,
|
|
106
|
+
)
|
|
107
|
+
generous = auto_tile_shape_cellpose(
|
|
108
|
+
(128, 2048, 2048),
|
|
109
|
+
"uint16",
|
|
110
|
+
diameter=30,
|
|
111
|
+
do_3D=True,
|
|
112
|
+
use_gpu=True,
|
|
113
|
+
gpu_memory=24 * GIB,
|
|
114
|
+
available_memory=64 * GIB,
|
|
115
|
+
)
|
|
116
|
+
assert np.prod(tile) < np.prod(generous), (
|
|
117
|
+
"the 1 GiB host grant must shrink the tile below what the same "
|
|
118
|
+
"24 GiB GPU would otherwise allow"
|
|
119
|
+
)
|
|
@@ -389,3 +389,40 @@ def test_channel_selection_respects_the_stores_axes(tmp_path):
|
|
|
389
389
|
store4 = str(tmp_path / "czyx.zarr")
|
|
390
390
|
to_ome_zarr(vol4, store4, axes="czyx", n_levels=1, progress=False)
|
|
391
391
|
assert load_ome_zarr(store4, channel=1, level=0).shape == (8, 64, 64)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_auto_tile_shape_charges_for_extra_channels():
|
|
395
|
+
"""A 2-channel tile must fit the same byte budget, not twice it.
|
|
396
|
+
|
|
397
|
+
`nuclei_channel` doubles what a tile holds while the tile geometry stays
|
|
398
|
+
single-channel, so a sizer blind to it hands the GPU a tile needing twice
|
|
399
|
+
the VRAM it budgeted for.
|
|
400
|
+
"""
|
|
401
|
+
import pytest
|
|
402
|
+
|
|
403
|
+
from patchworks import auto_tile_shape, auto_tile_shape_cellpose
|
|
404
|
+
|
|
405
|
+
shape, dtype = (128, 2048, 2048), "uint16"
|
|
406
|
+
|
|
407
|
+
one = auto_tile_shape(shape, dtype)
|
|
408
|
+
two = auto_tile_shape(shape, dtype, n_channels=2)
|
|
409
|
+
# Same bytes overall: 2 channels of roughly half the area each.
|
|
410
|
+
assert np.prod(two) * 2 <= np.prod(one)
|
|
411
|
+
assert np.prod(two) * 2 >= np.prod(one) * 0.9
|
|
412
|
+
|
|
413
|
+
kw = dict(
|
|
414
|
+
diameter=30,
|
|
415
|
+
do_3D=True,
|
|
416
|
+
use_gpu=True,
|
|
417
|
+
gpu_memory=24 * 1024**3,
|
|
418
|
+
# Generous on purpose: this test is about GPU-vs-channel scaling,
|
|
419
|
+
# not the host-RAM ceiling, so host RAM must stay non-binding here.
|
|
420
|
+
available_memory=64 * 1024**3,
|
|
421
|
+
)
|
|
422
|
+
cp_one = auto_tile_shape_cellpose(shape, dtype, **kw)
|
|
423
|
+
cp_two = auto_tile_shape_cellpose(shape, dtype, n_channels=2, **kw)
|
|
424
|
+
assert np.prod(cp_two) * 2 <= np.prod(cp_one)
|
|
425
|
+
assert cp_two[0] == cp_one[0] # do_3D still pins z to the full extent
|
|
426
|
+
|
|
427
|
+
with pytest.raises(ValueError, match="n_channels"):
|
|
428
|
+
auto_tile_shape(shape, dtype, n_channels=0)
|
|
@@ -158,3 +158,30 @@ def test_occupancy_is_not_rebuilt_by_the_driver():
|
|
|
158
158
|
src = (_workflow_dir() / "scripts" / "run_multi.py").read_text()
|
|
159
159
|
assert "build_occupancy_map(" not in src
|
|
160
160
|
assert "occupancy.zarr" in src
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_auto_tile_shape_with_a_lone_nuclei_channel_is_refused():
|
|
164
|
+
"""Matching `tile_shape` *values* are not enough when one config is 2-ch.
|
|
165
|
+
|
|
166
|
+
"auto" == "auto" passes the plain equality check, but the sizer charges
|
|
167
|
+
per channel, so the nuclei_channel config gets a smaller tile. The label
|
|
168
|
+
groups then disagree on chunk layout and label_relations raises -- after
|
|
169
|
+
every segmentation has already run, which is the expensive way to find out.
|
|
170
|
+
"""
|
|
171
|
+
paths = [Path("a.yaml"), Path("b.yaml")]
|
|
172
|
+
base = {"work_dir": "/w", "tile_shape": "auto", "level": 0}
|
|
173
|
+
|
|
174
|
+
bad = [
|
|
175
|
+
{**base, "label_name": "a", "channel": 0, "nuclei_channel": 1},
|
|
176
|
+
{**base, "label_name": "b", "channel": 2},
|
|
177
|
+
]
|
|
178
|
+
with pytest.raises(SystemExit):
|
|
179
|
+
_validate_configs(paths, bad)
|
|
180
|
+
|
|
181
|
+
# Same pair with one explicit shape is fine: both get that tile.
|
|
182
|
+
pinned = [{**c, "tile_shape": [16, 512, 512]} for c in bad]
|
|
183
|
+
assert _validate_configs(paths, pinned) == "/w"
|
|
184
|
+
|
|
185
|
+
# And "auto" is fine when every config carries the same channel count.
|
|
186
|
+
both = [{**bad[0]}, {**bad[1], "nuclei_channel": 3}]
|
|
187
|
+
assert _validate_configs(paths, both) == "/w"
|
|
@@ -68,6 +68,11 @@ Edit `config/config.yaml` (input, output dir, channel, tile shape, Cellpose
|
|
|
68
68
|
model/diameter/`do_3D`, …) and `profile/slurm/config.yaml` (partitions,
|
|
69
69
|
account, GPU request).
|
|
70
70
|
|
|
71
|
+
Channel indices are **0-based**. Set `nuclei_channel` alongside `channel` to
|
|
72
|
+
hand Cellpose the nuclear stain as a second input, which usually improves
|
|
73
|
+
cytoplasm segmentation — see
|
|
74
|
+
[Giving Cellpose a nuclei channel](https://imcf.one/patchworks/guide/snakemake/#giving-cellpose-a-nuclei-channel).
|
|
75
|
+
|
|
71
76
|
## Run
|
|
72
77
|
|
|
73
78
|
```bash
|
|
@@ -14,9 +14,11 @@ channel: 0 # cytoplasm/membrane channel
|
|
|
14
14
|
#
|
|
15
15
|
# Only the segment step reads it: the tile geometry, the occupancy map and the
|
|
16
16
|
# staged labels all stay exactly as they are for a single channel -- the pair
|
|
17
|
-
# is stacked on a leading axis that is carried into each tile, not tiled.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# is stacked on a leading axis that is carried into each tile, not tiled.
|
|
18
|
+
#
|
|
19
|
+
# It does double the bytes a tile holds. `tile_shape: "auto"` is told about
|
|
20
|
+
# that and shrinks each spatial side by ~1/sqrt(2) to fit the same VRAM
|
|
21
|
+
# budget; the hand-set tile_shape in common.yaml is not, so halve it yourself.
|
|
20
22
|
#
|
|
21
23
|
# Cellpose 3 turns this into channels: [1, 2]; Cellpose 4 (cpsam) dropped
|
|
22
24
|
# `channels` and just reads both. Override either by setting `channels:` or
|
|
@@ -67,6 +67,13 @@ set-resources:
|
|
|
67
67
|
prepare:
|
|
68
68
|
# Cheap now: it reduces the already-built occupancy map over each tile's
|
|
69
69
|
# footprint instead of touching the image.
|
|
70
|
+
#
|
|
71
|
+
# tile_shape: "auto" now also sizes against a host-RAM budget, and it
|
|
72
|
+
# reads *this job's* grant as a stand-in for segment's -- prepare runs on
|
|
73
|
+
# a CPU node and can't query segment's GPU-node grant directly. Keep this
|
|
74
|
+
# equal to segment's mem_mb below: lower and tiles come out needlessly
|
|
75
|
+
# small; higher and the sizer budgets against more host RAM than segment
|
|
76
|
+
# will actually get, undoing the point of the host-RAM check.
|
|
70
77
|
mem_mb: "attempt * 32000"
|
|
71
78
|
runtime: 120
|
|
72
79
|
segment:
|
|
@@ -80,7 +87,8 @@ set-resources:
|
|
|
80
87
|
# A job now processes `tiles_per_job` tiles sequentially, so both memory
|
|
81
88
|
# and runtime scale with that setting — raise it there and re-check here.
|
|
82
89
|
# The old "a tile used ~1G" note predates tile_shape: "auto", which sizes
|
|
83
|
-
# tiles against the real GPU and makes them far bigger.
|
|
90
|
+
# tiles against the real GPU and makes them far bigger. If you raise
|
|
91
|
+
# this, raise prepare's mem_mb above to match (see its comment).
|
|
84
92
|
mem_mb: "attempt * 32000"
|
|
85
93
|
cpus_per_task: 4
|
|
86
94
|
runtime: 360 # 6 hours — must match the QOS, NOT 120 (=2h → killed early)
|
|
@@ -44,6 +44,10 @@ if ts == "auto":
|
|
|
44
44
|
# "GPU memory query failed" fallback). None => the built-in 8 GiB default.
|
|
45
45
|
gpu_gb = cfg.get("gpu_memory_gb")
|
|
46
46
|
gpu_bytes = int(gpu_gb * 1024**3) if gpu_gb else None
|
|
47
|
+
# `image` is single-channel here (the geometry is spatial), but segment
|
|
48
|
+
# reads nuclei_channel alongside it, so a tile costs twice the bytes.
|
|
49
|
+
# Without this the sizer would hand the GPU a tile it cannot hold.
|
|
50
|
+
n_channels = 2 if cfg.get("nuclei_channel") is not None else 1
|
|
47
51
|
if method == "cellpose":
|
|
48
52
|
cp = cfg["cellpose"]
|
|
49
53
|
sizer = partial(
|
|
@@ -52,6 +56,7 @@ if ts == "auto":
|
|
|
52
56
|
use_gpu=cp.get("gpu", True),
|
|
53
57
|
diameter=cp.get("diameter"),
|
|
54
58
|
gpu_memory=gpu_bytes,
|
|
59
|
+
n_channels=n_channels,
|
|
55
60
|
)
|
|
56
61
|
else:
|
|
57
62
|
# cfg["cellpose"] used to be read unconditionally here, so a DoG or
|
|
@@ -59,7 +64,10 @@ if ts == "auto":
|
|
|
59
64
|
# 'cellpose'. The Cellpose estimator's memory model wouldn't apply to
|
|
60
65
|
# them anyway.
|
|
61
66
|
sizer = partial(
|
|
62
|
-
auto_tile_shape,
|
|
67
|
+
auto_tile_shape,
|
|
68
|
+
use_gpu=gpu_bytes is not None,
|
|
69
|
+
gpu_memory=gpu_bytes,
|
|
70
|
+
n_channels=n_channels,
|
|
63
71
|
)
|
|
64
72
|
tile_shape = tuple(sizer(image.shape, image.dtype))
|
|
65
73
|
else:
|
|
@@ -228,6 +228,24 @@ def _validate_configs(paths: list[Path], cfgs: list[dict]) -> str:
|
|
|
228
228
|
f"share a chunk layout; got {_spread(key)}"
|
|
229
229
|
)
|
|
230
230
|
|
|
231
|
+
# `tile_shape: "auto"` is identical as a *value* across configs while
|
|
232
|
+
# producing different tiles: the sizer charges per channel, so a config
|
|
233
|
+
# with nuclei_channel gets a smaller one. The label groups then disagree on
|
|
234
|
+
# chunk layout and label_relations raises -- after every segmentation has
|
|
235
|
+
# run. Matching values are not enough here, so check the inputs that feed
|
|
236
|
+
# the sizer instead.
|
|
237
|
+
if {repr(cfg.get("tile_shape", "auto")) for cfg in cfgs} == {repr("auto")}:
|
|
238
|
+
if len({cfg.get("nuclei_channel") is not None for cfg in cfgs}) != 1:
|
|
239
|
+
problems.append(
|
|
240
|
+
'tile_shape: "auto" sizes a nuclei_channel config smaller '
|
|
241
|
+
"(a tile carries two channels), so the label groups would "
|
|
242
|
+
"not share a chunk layout and label_relations would fail "
|
|
243
|
+
f"after every segmentation had run; got "
|
|
244
|
+
f"{_spread('nuclei_channel')}. Set one explicit tile_shape in "
|
|
245
|
+
"the file `common:` points at, sized for the two-channel "
|
|
246
|
+
"config."
|
|
247
|
+
)
|
|
248
|
+
|
|
231
249
|
# Phase A converts once, from the first config. Anything `convert` reads
|
|
232
250
|
# out of a later config is therefore silently ignored -- someone setting
|
|
233
251
|
# `shard: true` on the second config and watching a million files appear
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|