Skip to content

Commit bed0daf

Browse files
lguerardclaude
andcommitted
perf(merge): ⚡ derive global ids from tile counts, dropping two full passes
The merge rewrote the whole staged store twice for bookkeeping. Pass 0 (_make_globally_unique) streamed every chunk and renumbered it in place, so tile-local 1..N ids stopped colliding. relabel_sequential_zarr then made a second full read+write to compact the ids, building a Python set of every label id on the way (~60 bytes each). Neither pass needs to touch the volume. stage_tile already knows how many labels it wrote, so it now renumbers its trimmed output densely and returns that count; the segment job records it in the .done marker, which until now was an empty file. Global ids are then offset + local with offset an exclusive cumulative sum over those counts -- O(n_tiles) arithmetic. The offsets are applied where they are needed instead: on the two chunks either side of each boundary during the scan, and per chunk in the relabel worker. That makes the id domain dense by construction, so the sequential renumber becomes np.unique over the LUT -- length = object count, not voxels -- and folds into the lookup the relabel pass already performs. Passing no counts keeps the old streaming path, so the dask-array entry point and any external caller are unaffected. The relabel worker also stops widening whole blocks to int64, gathering only the non-zero ids instead; labels are sparse, so its peak drops accordingly. Verified on a synthetic workflow run: byte-identical labels and object count to the previous implementation, and a test asserts the two paths agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent bbf7da2 commit bed0daf

5 files changed

Lines changed: 267 additions & 51 deletions

File tree

src/patchworks/_distributed.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import numpy as np
1919
import zarr
2020

21+
from ._relabel import relabel_sequential_array
22+
2123
Overlap = Union[int, Sequence[int]]
2224

2325

@@ -158,7 +160,11 @@ def stage_tile(
158160
Returns
159161
-------
160162
int
161-
The processed tile *index*.
163+
Number of labels this tile wrote, i.e. its ids are exactly ``1..n``.
164+
Record it (the workflow puts it in the tile's ``.done`` marker): with
165+
one count per tile the merge can derive every tile's global id range
166+
by a cumulative sum, instead of rewriting the whole store to make the
167+
ids unique.
162168
"""
163169
shape = image.shape
164170
sl = spatial_tiles(shape, tile_shape)[index]
@@ -175,8 +181,13 @@ def stage_tile(
175181
slice(left, out.shape[i] - right)
176182
for i, (left, right) in enumerate(trims)
177183
)
178-
# Local labels (1..N) are fine here — they collide across tiles, but the
179-
# merge's first pass makes them globally unique before stitching.
184+
trimmed = out[sel]
185+
# Local labels collide across tiles; the merge resolves that. Renumber to a
186+
# dense 1..n first: trimming the halo can drop objects entirely, and the
187+
# merge's offset arithmetic needs each tile's ids to be exactly 1..n with
188+
# no gaps so that `offset[tile] + local` is globally unique AND compact.
189+
trimmed = relabel_sequential_array(trimmed)
190+
n_labels = int(trimmed.max())
180191
dst = zarr.open_group(str(stage_path), mode="r+")[component]
181-
dst[sl] = out[sel].astype(dst.dtype)
182-
return index
192+
dst[sl] = trimmed.astype(dst.dtype)
193+
return n_labels

0 commit comments

Comments
 (0)