Repository navigation
n4_biasfield memory grows with image size instead of streaming #215
Copy link
Copy link
Open
Description
n4_biasfield applies a precomputed low-resolution N4 bias field to the image at the segmentation level. It should stream block by block, but its peak memory grows with the size of the image. Right now it is the largest memory request in the workflow, and it fails on the first attempt on some subjects.
Measurements
All runs used 128 threads, on .ims inputs from trident_rescans (benchmarks from #213 and #214):
| Subject | Level | Image (one channel) | Wall time | Peak memory |
|---|---|---|---|---|
| F1A1Te4 | 0 | 304 GB | 49 min | 162 GB |
| M1A1Te3 | 0 | 563 GB | 62 min | 333 GB |
| M1A1Te3 | 1 | 70 GB | 9 min | 130 GB |
| F1A1Te4 | 1 | 38 GB | 14 min | 114 GB (172 GB by Slurm) |
- On the first attempt of the last row (147 GB requested), dask workers kept dying (
KilledWorkeron_fused_blocktasks). It succeeded on the retry with 294 GB. CPU efficiency was about 5%. - The current request,
scaled_mem_mb(128000, 500), is fitted to these numbers: a large fixed base plus about 0.5× the image size.
Where the memory might go
These are untested candidates, in spimquant/workflow/scripts/n4_biasfield.py and ZarrNii 0.21.6:
- Round trip through a temporary zarr for
.ims:znimg_in.to_ome_zarr(temp_dir)usestempfile.TemporaryDirectory(), which is/tmpby default. If/tmpis RAM-backed on the compute nodes, the whole image ends up counted against the job's memory. - Writing the pyramid:
to_ome_zarr(..., match_scale_factors_from=...)writes level 0 plus downsampled levels. If the levels are computed from one shared graph, dask may keep level-0 blocks in memory until every level is written. - Per-block interpolation in
ZarrNii._apply_scaled_processing_map_blocks:scipy.ndimage.map_coordinatesbuilds a coordinate grid of 3 × voxels in float64 for each block, about 24 bytes per voxel. It does this twice when a mask is given. For a 512³ block that is about 3 GB per call. Withthreads_per_worker=16for .ims, a worker runs 16 blocks at once, about 48 GB, whilememory_limit="auto"gives each of the 8 workers 18 GB of a 147 GB job. The scaling is axis-aligned, so separable 1-D linear interpolation would need only a few 1-D weight vectors per axis. - dask settings:
threads_per_worker(16 for .ims, 2 otherwise) and the chunk sizes on read (256³) and write (target_chunk_size=512).
Plan
- Reproduce on one subject at level 1, then measure the steps one at a time: read only, then apply the bias field without writing, then write only level 0, then write the full pyramid. Use
--benchmarkand the dask dashboard or a memory profile. Check whether/tmpis tmpfs on the nodes. - Fix in ZarrNii whatever turns out to be ZarrNii's (likely items 2 and/or 3): separable interpolation in
map_blocks, and a pyramid write that streams. Open a ZarrNii issue or PR once the cause is known. - Fix in SPIMquant whatever belongs to the script: write the
.imstemp zarr to the job's disk scratch (resources.tmpdir) or skip it, and tunethreads_per_workerand the chunk sizes. - Refit
n4_biasfieldresources once memory follows threads × chunk size. Runtools/summarize_benchmarks.pyon runs at levels 0 and 1, and compare with what Slurm measures (--slurm-efficiency-report), since Slurm reports more than psutil. - Check the other dask rules for the same pattern. When the request is too small, workers pause at their per-worker limit, so the benchmarked peak stays low and hides it.
compute_filtered_regionpropswas raised for this reason in Group the short jobs of each subject into one cluster job per stage #214.signed_distance_transformalso had workers die on its first attempt.
Done when
- Peak memory no longer grows with image size at a fixed chunk size and thread count.
n4_biasfieldsucceeds on the first attempt at levels 0 and 1 with a request well under the current one.
Metadata
Metadata
Assignees
Labels
No labels
Activity
akhanf commentedon Oct 27, 2026
Status: both "done when" criteria are met with khanlab/zarrnii#271 and #216.
Cluster runs with the new request (128 threads, 96 GB; dask gives each worker 12 GB). The table shows the apply + pyramid stage:
Peak memory per worker is the same at levels 0 and 1. There were three causes, all in ZarrNii and all fixed in khanlab/zarrnii#271:
#216 can leave draft once a ZarrNii release includes #271 and the pin is raised.
Separately, OME-Zarr outputs written with ome-zarr 0.18 (in the lock since
0b45135) have lost their voxel size, origin and units. That is tracked in khanlab/zarrnii#272.🤖 Generated with Claude Code