patchworks 2.6.1__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.1 → patchworks-2.6.2}/PKG-INFO +1 -1
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/gpu_distributed.md +5 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/snakemake.md +31 -2
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/tiling.md +17 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_chunks.py +33 -7
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_allocation.py +42 -1
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_core.py +9 -1
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_run_multi.py +27 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/README.md +5 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/profile/slurm/config.yaml +9 -1
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/run_multi.py +18 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/docs.yml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/lint.yml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/.github/workflows/release.yml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/.gitignore +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/.markdownlint-cli2.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/LICENSE +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/README.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/cliff.toml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/chunks.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/cluster.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/io.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/dog.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/napari.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/postprocess.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/relabel.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/api/tile_process.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/assets/logo.png +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/custom.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/custom_method.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/dog.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/dog.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/standalone_merge.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/stardist.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/examples/stardist_2d.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/getting_started.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/custom_segmentation.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/label_relations.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/measurements.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/merging.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/performance.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/pitfalls.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/guide/skip_empty.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/docs/index.md +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/mkdocs.yml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/pyproject.toml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/__init__.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_cluster.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_core.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_distributed.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_gpu.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_io.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_merge.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_notify.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_occupancy.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_postprocess.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_progress.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_relabel.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/_relations.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/dog.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_distributed.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_dog.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_gpu.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_napari.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_notify.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_occupancy.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_ome_zarr.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_postprocess.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_progress.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/tests/test_relations.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/Snakefile +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/common.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/config/multi.yaml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/pixi.toml +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/common.smk +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/convert.smk +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/merge.smk +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/rules/segment.smk +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/_pw.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/build_occupancy.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/convert.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/merge.py +0 -0
- {patchworks-2.6.1 → patchworks-2.6.2}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-2.6.1 → 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×
|
|
@@ -323,13 +332,33 @@ follow from that:
|
|
|
323
332
|
|
|
324
333
|
- A tile holds twice the bytes. `tile_shape: "auto"` accounts for this — it is
|
|
325
334
|
told the tile carries two channels and shrinks each spatial side by ~1/√2,
|
|
326
|
-
so the tile still fits the same VRAM budget
|
|
327
|
-
|
|
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.
|
|
328
338
|
- The translation is version-specific. Cellpose 3 gets `channels: [1, 2]`
|
|
329
339
|
(1-based into the channel axis, `0` = grayscale); Cellpose 4 (cpsam) dropped
|
|
330
340
|
`channels` entirely and simply reads both. Either is overridable by setting
|
|
331
341
|
`channels:` or `channel_axis:` in the `cellpose:` block.
|
|
332
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
|
+
|
|
333
362
|
`nuclei_channel` applies to the SLURM/Snakemake path. The single-process
|
|
334
363
|
`tile_process` API still takes one `channel`.
|
|
335
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
|
|
@@ -281,11 +281,15 @@ def auto_tile_shape(
|
|
|
281
281
|
target_bytes:
|
|
282
282
|
Memory ceiling per tile. Default 64 MiB.
|
|
283
283
|
use_gpu:
|
|
284
|
-
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``.
|
|
285
286
|
gpu_memory:
|
|
286
287
|
Available GPU VRAM in bytes; auto-queried when None.
|
|
287
288
|
available_memory:
|
|
288
|
-
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.
|
|
289
293
|
n_workers:
|
|
290
294
|
Number of parallel workers (divides the RAM budget).
|
|
291
295
|
n_channels:
|
|
@@ -316,7 +320,13 @@ def auto_tile_shape(
|
|
|
316
320
|
n_spatial = min(3, len(shape))
|
|
317
321
|
|
|
318
322
|
if use_gpu:
|
|
319
|
-
|
|
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)
|
|
320
330
|
budget = min(target_bytes * 2, mem // 2)
|
|
321
331
|
else:
|
|
322
332
|
mem = available_memory or _get_available_memory()
|
|
@@ -395,13 +405,23 @@ def auto_tile_shape_cellpose(
|
|
|
395
405
|
do_3D:
|
|
396
406
|
Whether Cellpose will run in 3-D mode.
|
|
397
407
|
use_gpu:
|
|
398
|
-
Size tiles for GPU VRAM.
|
|
408
|
+
Size tiles for GPU VRAM. Still capped by host RAM too -- see
|
|
409
|
+
``available_memory``.
|
|
399
410
|
gpu_memory, available_memory, n_workers:
|
|
400
|
-
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.
|
|
401
416
|
model_memory_bytes:
|
|
402
417
|
Memory consumed by the Cellpose model weights (default 2 GiB).
|
|
403
418
|
cellpose_memory_factor:
|
|
404
|
-
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.
|
|
405
425
|
n_channels:
|
|
406
426
|
Channels each tile carries (default 1). Above 1 the per-voxel cost
|
|
407
427
|
scales with it, so the tile shrinks accordingly -- e.g. the workflow's
|
|
@@ -429,7 +449,13 @@ def auto_tile_shape_cellpose(
|
|
|
429
449
|
itemsize = np.dtype(dtype).itemsize * n_channels
|
|
430
450
|
|
|
431
451
|
if use_gpu:
|
|
432
|
-
|
|
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)
|
|
433
459
|
else:
|
|
434
460
|
total_mem = (available_memory or _get_available_memory()) // n_workers
|
|
435
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
|
+
)
|
|
@@ -410,7 +410,15 @@ def test_auto_tile_shape_charges_for_extra_channels():
|
|
|
410
410
|
assert np.prod(two) * 2 <= np.prod(one)
|
|
411
411
|
assert np.prod(two) * 2 >= np.prod(one) * 0.9
|
|
412
412
|
|
|
413
|
-
kw = dict(
|
|
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
|
+
)
|
|
414
422
|
cp_one = auto_tile_shape_cellpose(shape, dtype, **kw)
|
|
415
423
|
cp_two = auto_tile_shape_cellpose(shape, dtype, n_channels=2, **kw)
|
|
416
424
|
assert np.prod(cp_two) * 2 <= np.prod(cp_one)
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|