Skip to content

Commit 8cef606

Browse files
lguerardclaude
andcommitted
fix(convert): πŸ› warn when fine chunks mean a million files
Capping the output chunk by the source's granularity is what stops the conversion OOMing, but it has a cost worth naming: for the stitched-TIFF case level 0 goes from ~60k chunks to ~950k, and without sharding a chunk is a file. That lands on a shared filesystem at write time and on every read afterwards. Sharding fixes it without touching the chunking -- and safely, because _auto_shard grows the two largest axes, which are y and x, so a shard still lives inside one source plane and needs no more memory than a chunk did. For the real dataset that is 4,536 files instead of 952,560. So the write now says so, with the setting to change, rather than leaving it to be discovered later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 34debac commit 8cef606

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

β€Žsrc/patchworks/plugins/ome_zarr.pyβ€Ž

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,37 @@ def _one(idx: tuple[int, ...]) -> None:
466466
pass
467467

468468

469+
# One chunk is one file without sharding. A shared cluster filesystem starts
470+
# to hurt well before a million of them, and the cost lands on every later
471+
# read too, not just the write.
472+
_CHUNK_COUNT_WARN = 200_000
473+
474+
475+
def _warn_if_many_chunks(
476+
shape: tuple[int, ...], chunks: tuple[int, ...], shard: ShardSpec
477+
) -> None:
478+
"""Warn when a level will be written as a very large number of files.
479+
480+
A coarse source forces fine output chunks (see :func:`_default_chunks`),
481+
and fine chunks mean many files. Sharding packs them into far fewer
482+
without changing the chunking, so the fix is cheap -- but only if someone
483+
notices before the store is written.
484+
"""
485+
if shard:
486+
return
487+
count = int(np.prod([math.ceil(s / c) for s, c in zip(shape, chunks)]))
488+
if count < _CHUNK_COUNT_WARN:
489+
return
490+
logger.warning(
491+
"level 0 will be %s chunks, i.e. that many files: the source's own "
492+
"granularity forced chunks of %s. Set shard=True (config: "
493+
"shard: true) to pack them into far fewer files -- same chunking, "
494+
"same memory, and kinder to a shared filesystem.",
495+
f"{count:,}",
496+
chunks,
497+
)
498+
499+
469500
def _bounded_scheduler(arr: da.Array):
470501
"""Cap dask's threads by the memory one **source** chunk costs.
471502
@@ -706,6 +737,7 @@ def _write_pyramid(
706737
base_chunks = chunks or _default_chunks(
707738
arr.shape, axes, source_chunks=arr.chunksize
708739
)
740+
_warn_if_many_chunks(arr.shape, base_chunks, shard)
709741
_to_zarr_level(
710742
arr.rechunk(base_chunks), group_path, base_name, shard, progress
711743
)

β€Žtests/test_ome_zarr.pyβ€Ž

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,3 +550,29 @@ def test_uncalibrated_tiff_reports_nothing(tmp_path):
550550
path = tmp_path / "bare.tif"
551551
tifffile.imwrite(path, np.zeros((8, 8), "uint16"))
552552
assert _tiff_pixel_size(str(path)) == {}
553+
554+
555+
def test_many_chunks_suggests_sharding(caplog):
556+
"""A coarse source forces fine chunks; fine chunks mean many files.
557+
558+
That trade-off is invisible until a shared filesystem is carrying a
559+
million of them, so it has to be said at write time -- with the remedy.
560+
"""
561+
import logging
562+
563+
from patchworks.plugins.ome_zarr import _warn_if_many_chunks
564+
565+
real = (4, 126, 45961, 42072) # ~950k chunks at (1, 1, 1024, 1024)
566+
with caplog.at_level(logging.WARNING, logger="patchworks.plugins.ome_zarr"):
567+
_warn_if_many_chunks(real, (1, 1, 1024, 1024), shard=False)
568+
assert "shard: true" in caplog.text
569+
assert "952,560" in caplog.text
570+
571+
# Sharding already asked for, or a modest chunk count: nothing to say.
572+
caplog.clear()
573+
with caplog.at_level(logging.WARNING, logger="patchworks.plugins.ome_zarr"):
574+
_warn_if_many_chunks(real, (1, 1, 1024, 1024), shard=True)
575+
_warn_if_many_chunks(
576+
(4, 126, 1024, 1024), (1, 16, 1024, 1024), shard=False
577+
)
578+
assert caplog.text == ""

0 commit comments

Comments
Β (0)