Skip to content

feat(nextflow): self-heal torn-shard/IO failures on per-position steps + default to non-preemptible cpu partition#285

Open
tayllatheodoro wants to merge 5 commits into
mainfrom
nextflow-checksum-self-heal
Open

feat(nextflow): self-heal torn-shard/IO failures on per-position steps + default to non-preemptible cpu partition#285
tayllatheodoro wants to merge 5 commits into
mainfrom
nextflow-checksum-self-heal

Conversation

@tayllatheodoro

@tayllatheodoro tayllatheodoro commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the recurrent zarr torn-shard corruption that kept aborting mantis-v2 per-position steps and looping on -resume (root-cause writeup: #286). Two complementary changes:

1. Generalized per-position self-heal

checksum_heal(output_zarr, position, cmd) in nextflow/modules/common.nf, wired into run_flat_field, run_deskew, run_apply_inv_tf, run_virtual_stain.

A per-position task killed mid-write (SLURM preemption / timeout / OOM, or a transient storage EIO) leaves a torn/partial zarr v3 shard. These steps write partial chunks via read-modify-write, so the next attempt (Nextflow retry or -resume) reads the torn shard back and the zarrs codec aborts with a non-signal exit (1) that the global errorStrategy would terminate on. The same root cause surfaces as several different messages depending on the codec:
the checksum is invalid / encoded shard is smaller than the expected size / blosc encoded value is invalid / …

Rather than enumerate every message, the wrapper heals on any non-zero exit: because a per-position task always fully recomputes its position from the input, it is always safe to clear that position's chunk data (the c/ dirs, preserving the zarr.json scaffold from --init) and run the command once more. If the retry also fails, its exit status propagates (genuine bugs are not masked — they just cost one extra attempt). No-op on success; scoped to a single position group so concurrent tasks never collide. This also gives transient EIO a second chance (the EIO-retry idea from #286).

2. Route cpu steps off the preempted partition

nextflow/nextflow.config: withLabel: 'cpu' now queues on the non-preemptible cpu partition instead of preempted. Preemptive reclamation mid-write was the primary source of torn shards; running on cpu removes it. gpu is unchanged. Switching back to preempted remains a one-line change for cost-sensitive runs.

Commit history (evolution)

  • feat: self-heal checksum is invalid
  • fix: also encoded shard is smaller than the expected size
  • fix: default cpu steps to non-preemptible cpu partition
  • fix: also blosc encoded value is invalid
  • refactor: generalize to clean-and-retry on any failure (supersedes the string matching)

Verification

  • nextflow lint clean on all changed files.
  • Heal logic tested under bash -ue (Nextflow's shell): success is a no-op; a torn shard (incl. blosc) is cleaned + retried → success; a genuine bug is cleaned + retried once then propagates; transient EIO gets a retry.
  • Validated in production on 2026_07_14_A549_MAP4_ZIKV: healed checksum / encoded-shard / blosc positions across flat_field & deskew; on cpu there are zero preemption (exit-143) kills, and runs pass flat_field → deskew → reconstruct.

Refs #286, #287, czbiohub-sf/iohub#415, czbiohub-sf/iohub#442, zarrs/zarrs#420.

tayllatheodoro and others added 2 commits July 20, 2026 10:14
…steps

A per-position task killed mid-write (SLURM preemption/timeout/OOM — the
130..145 signal exits the global errorStrategy retries) can leave a torn
zarr v3 shard in the output. flat-field/deskew/apply-inv-tf/virtual-stain
write partial chunks via read-modify-write, so the next attempt (a Nextflow
retry or a later -resume) reads the torn shard back and zarr raises
`RuntimeError: the checksum is invalid` — a non-signal exit (1) that the
errorStrategy terminates on, so retries and -resume both keep dying on the
same position until the chunk data is manually deleted.

Add a `checksum_heal(output_zarr, position, cmd)` helper in common.nf that
wraps each per-position command: on failure it inspects the log and ONLY if
it sees "checksum is invalid" removes that position's chunk data (zarr v3
`c/` dirs, preserving every zarr.json scaffold from --init) and re-runs the
command once. Any other failure is re-raised unchanged for the existing
errorStrategy. No-op on success; scoped to a single position group so
concurrent tasks never collide. Heals both the retry and -resume cases.

Wired into run_flat_field, run_deskew, run_apply_inv_tf, run_virtual_stain.
Out of scope: whole-plate single-shot steps (compute_transfer_function,
run_concatenate), which have no per-position key.

Closes #284

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rror

The same partial-write corruption (a shard left torn/truncated by a killed
mid-write task) surfaces from the zarrs codec pipeline as two distinct
messages, both on the read-modify-write path:
  - "the checksum is invalid"                     (CRC32C mismatch)
  - "encoded shard is smaller than the expected size of its index."
                                                  (truncated shard)

checksum_heal only matched the first, so a run could still terminate on the
second (observed on a MAP4/ZIKV flat_field resume, position B/3/000001).
Broaden the detection to an extended-regex matching both signatures; the
remedy is identical (clear the position's chunk data, keep the zarr.json
scaffold, retry once). See czbiohub-sf/iohub#415.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tayllatheodoro and others added 3 commits July 20, 2026 16:59
The per-position compute steps ran on `preempted`, where SLURM reclaims jobs
mid-write (SIGTERM, exit 143) and leaves partially-written zarr shards. Those
torn shards then fail every subsequent read-modify-write ("checksum is invalid"
/ "encoded shard is smaller than the expected size") and keep aborting on
-resume — the primary driver of the corruption in #286/#287.

Route `withLabel: 'cpu'` to the non-preemptible `cpu` partition instead, which
removes the mid-write-kill source. `gpu` is unchanged. The errorStrategy retry
range still covers genuine time-limit/OOM kills. Switching back to `preempted`
remains a one-line change for cost-sensitive runs.

Refs #286, #287

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hards

A third torn-shard symptom from the zarrs write/RMW path: when the corrupt
inner chunk is blosc-compressed, decoding the torn shard raises
`RuntimeError: blosc encoded value is invalid` (rather than the CRC32C
"checksum is invalid" or the "encoded shard is smaller ..." truncation).
Observed on B/5/003000 after an interrupted (EIO) write left a torn shard;
the heal pattern didn't match it, so the run aborted on -resume.

Add it to the torn-shard regex; remedy is identical (clear the position's
chunk data, keep the zarr.json scaffold, retry once). Refs #286, #442, iohub#415.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ilure

Torn shards surfaced as an open-ended set of zarrs codec messages (checksum
is invalid / encoded shard is smaller / blosc encoded value is invalid / ...),
each requiring a new string match. Since a per-position task always fully
recomputes its position from the input, clearing that position's chunk data
and retrying once is safe for ANY failure. Heal on any non-zero exit instead
of matching error text: recovers every torn-shard flavour (present and future)
and gives transient storage errors (EIO) a second chance, while a genuine bug
just fails the retry and propagates as before. Refs #286, #442, iohub#415.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tayllatheodoro tayllatheodoro changed the title feat(nextflow): self-heal zarr "checksum is invalid" on per-position steps feat(nextflow): self-heal torn-shard/IO failures on per-position steps + default to non-preemptible cpu partition Jul 21, 2026
@tayllatheodoro

Copy link
Copy Markdown
Collaborator Author

Errors encountered & decision log

Full record of the errors that drove this PR and why each change was made. Root-cause tracker: #286.

Errors encountered (in the order they surfaced)

# Error Where / trigger Root cause Resolution
1 RuntimeError: the checksum is invalid flat_field write (RMW), exit 1 CRC32C shard-index mismatch reading a torn shard left by a killed write self-heal: clean chunks + retry
2 exit 143 (SIGTERM) per-position tasks on preempted SLURM preemption/timeout reclaims job mid-write → torn shard (the source) move cpu steps off preempted
3 RuntimeError: The encoded shard is smaller than the expected size of its index. flat_field write (RMW), exit 1 truncated shard (same torn-shard root cause, different codec message) broadened heal pattern
4 RuntimeError: blosc encoded value is invalid flat_field write (RMW), exit 1 corrupt blosc inner chunk (same root cause, 3rd message) broadened heal → then generalized
5 RuntimeError: Input/output error (os error 5) (EIO) flat_field write, exit 1 transient storage (Lustre) error mid-write; also seeded torn shards in in-flight neighbours when it aborted the session generalized heal gives it a retry
6 exit 140 (SIGUSR2) C/4/000000, C/4/000002 near-walltime (chronically slow positions); retried via errorStrategy (time scales by attempt) noted; bump flat_field walltime if it persists
7 KeyError: 'median'RuntimeError: applying transform NormalizeSampled virtual-stain, exit 1 predict.yml used level: timepoint_statistics (nested per-timepoint), but biahub's in-process VS path (#267) doesn't pre-resolve the timepoint as viscy's dataloader does → not corruption predict.ymllevel: fov_statistics (unblock); code gap tracked

Key decisions (and why)

  1. Self-heal at the Nextflow layer, all per-position steps — chosen over a biahub Python change: contained to nextflow/modules/, fixes both the retry and -resume cases, and works for every per-position CLI without touching core write logic.
  2. Detect, then generalize — started by matching specific torn-shard strings (checksum → encoded-shard → blosc). After the 3rd string it was clearly whack-a-mole, so generalized to clean-and-retry on any non-zero exit. Safe because a per-position task always fully recomputes from input, so clearing its chunks is never destructive; a genuine bug just fails the retry and propagates (not masked). Also covers transient EIO for free.
  3. preemptedcpu partition — removes the source of new torn shards (mid-write preemption). Verified: zero exit-143 after the switch, vs 36 before. Kept as a one-line, reversible knob for cost-sensitive runs; filed Per-position steps hardcoded to preempted partition and not user-configurable #287 to make it configurable.
  4. Virtual-stain normalizationtimepoint_statistics is offered as a config option but unsupported by biahub's in-process VS path; quick unblock via fov_statistics, code fix (pre-resolve timepoint) left as a tracked gap.
  5. Issues filedRecurrent zarr torn-shard corruption on preemptible per-position steps (mantis-v2) #286 (parent root-cause), Per-position steps hardcoded to preempted partition and not user-configurable #287 (partition config), create_empty_plate shard/chunk geometry forces read-modify-write, making torn shards recur iohub#442 (shard geometry forces RMW), all cross-linked to RuntimeError: the checksum is invalid when writing zarr chunks via zarrs codec on Lustre iohub#415 and Add storage adapter that writes a tmp file then atomically moves on success zarrs/zarrs#420.

Process missteps & corrections (for transparency)

  • Editing the live repo checkout invalidated resume cache. The pipeline resolves code from this repo at runtime (--biahub_project); editing its working tree changed run_flat_field task hashes, so -resume re-ran all positions and the live run picked up un-merged branch code. Corrected by restoring main and being explicit about which branch runs use.
  • Orphaned preempted jobs after a cancel — a cancel left previous-run deskew jobs running unmanaged, risking two runs writing the same store. Cancelled the orphans before continuing.
  • Started from scratch once, to clear accumulated torn shards + cache confusion from many cancel/restart cycles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant