Skip to content

Worker: honour or refuse shared-memory order; memory-map big TIFF inputs - #125

Merged
guijacquemet merged 1 commit into
mainfrom
claude/worker-memmap-forder
Oct 13, 2026
Merged

guijacquemet merged 1 commit into
mainfrom
claude/worker-memmap-forder

Conversation

@guijacquemet

Copy link
Copy Markdown
Contributor

What and why

Two worker-side changes to how an image input is read, from the findings of #123 (docs/PERF_SHARED_MEMORY.md sections 2, 4.5 and 6).

1. Shared-memory order (correctness). The worker ignored a descriptor's order, so an F-order block (what an ImgLib2-style [X, Y] host naturally has) was read transposed with no error: wrong science. Now C/C_ORDER (or no key) and F/F_ORDER are honoured: the block is read with the strides of the declared order and the tool gets the same logical array, as a C-ordered copy. Any other value ("A", "K", "f", a number, a list, null) is refused with unreadable_image and a message saying send 'C' or 'F'. I chose honour-and-test because it is one order= argument; unknown values are refused either way. Documented in docs/PROTOCOL.md.

2. Memory-mapped TIFF input. An image input given as a TIFF file of at least 8 MiB that is clearly the simple case (one series, one level, uncompressed, untiled, every page's strips in one contiguous run, native byte order, kind generic/ImageJ/shaped, not cut short) is mapped with np.memmap(mode="c") instead of read and copied.

Behaviour changes

  • Tool-visible semantics are unchanged: same shape, dtype and values as tifffile.imread, a plain writable numpy.ndarray. Decision: copy-on-write, not a read-only view. A read-only view would make a tool that today does image[mask] = 0 on its input raise; copy-on-write keeps it working, and the first write copies the page privately so the file (the host's export or a person's own) never changes.
  • Fallback to the existing tifffile.imread path for compressed, tiled, predictor, multi-series, pyramid, non-contiguous, OME, non-native byte order, small (< 8 MiB), cut-short files, and when LC_TIFF_MEMMAP=0 is set in the worker's environment.
  • Found while testing: tifffile's series.dataoffset trusts the layout of a series from its first pages, so a stack whose later pages lie elsewhere would have been mapped to the wrong pixels. Every page's offsets are now checked (_pages_are_one_block; about 65 ms for a 5000-page stack), with a test on a patched file.
  • Lifetime: the worker calls convert.release_mapped_inputs() before every terminal message, so a failed tool whose traceback or a reference cycle holds the input does not keep the file locked (Windows) after FAILURE. Documented limits in PROTOCOL.md: a tool that keeps its input in a global, and a host that truncates the file while a task runs (a mapped page past the new end ends the worker).
  • Timing at 512 MiB (uint16, warm cache, Linux sandbox, median of 5), benchmarks/tiff_memmap.py from Research: shared memory for big images (issue #66), benchmarks and findings #123 and the real worker (load_inputs_s of COMPLETION):
before (imread) after (mapped)
#123 script, load 0.202 s 0.001 s
#123 script, load + one full pass 0.243 s 0.040 s
#123 script, cold cache, load + pass 0.298 s 0.218 s (the read moves into the first pass)
real worker, load_inputs_s 0.191 s 0.001 s
real worker, task wall time (load + max()) 0.232 s 0.050 s

Tests

  • tests/test_input_loading.py (+ tests/input_app/): F order read with a non-symmetric array in 2D and 3D, both spellings, C/absent, 10 refused values, through a real worker; mapped cases (u8/u16/i16/u32/f32/f64, stack, RGB, ImageJ hyperstack, BigTIFF, many strips) equal to imread in shape/dtype/values and plain ndarray type; every fallback (zlib, tiled, predictor, a stack with a gap, two series, OME, opposite byte order, pyramid, small, cut-short, env switch) with the same result as imread; a tool writing to its input (in process and through a worker): file hash and mtime unchanged, file deletable afterwards; after both a completed and a failing task the worker has no mapping of the file (/proc/self/maps) and the file can be removed.
  • Sabotage (tests/test_sabotage.py, 6 new breaks, all caught): order ignored, unknown order read as C, mapping opened r+, the page-gap check removed, mapping not released, mapping never used. Run locally with LC_SABOTAGE_ONLY.
  • Local: full suite 635 tests, 1 failure that also fails on main here (see below); black, ruff, mypy, vulture as in the lint job.

Not verified

  • Windows and macOS behaviour of the mapping (file locking after the task, copy-on-write, os.remove after FAILURE) is only exercised by the CI cells; nothing here ran on those systems. See CI below.
  • Cold-cache numbers are from drop_caches in a sandbox VM, not a spinning or network disk; no timing on real host machines or with Defender.
  • Hosts: nothing was changed or run in the Napari, Fiji or QuPath hosts. Fiji writes TIFFs with ImageJ that are covered by the ImageJ case only as written by tifffile (imagej=True).
  • test_tools_features.py::test_doctor_prints_them_and_does_not_call_an_upgrade_stale: not a failure on main in CI (green, full matrix, run 38356428622) nor in a clean virtualenv here. It fails when run with this sandbox's system Python: that interpreter has an older editable install of Tools (/tmp/wt_versions, same version string, without retention_prune) which the doctor subprocess imports instead of the checkout, so the printed feature list differs. An environment artefact, not changed here. (test_hardening...shared_entry_is_skipped fails on main too when run as root in this sandbox; it is skipped on CI runners.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN


Generated by Claude Code

… inputs

Shared-memory descriptors: the worker ignored `order`, so an F-order block was read
transposed with no error. It now reads C and F (also Appose's C_ORDER/F_ORDER) with the
right strides and refuses any other value with unreadable_image.

TIFF inputs: an uncompressed, contiguous, single-series TIFF of at least 8 MiB is mapped
copy-on-write instead of read. The tool gets the same shape, dtype and values in a plain
writable array; writes never reach the file. Every page's offsets are checked (tifffile's
series.dataoffset trusts a series' layout from its first pages). Compressed, tiled,
multi-series, pyramid, OME, non-native byte order, small and cut-short files are read as
before; LC_TIFF_MEMMAP=0 turns it off. Mappings are released before the terminal message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN
@guijacquemet
guijacquemet merged commit 70b5a95 into main Oct 13, 2026
14 checks passed
guijacquemet added a commit that referenced this pull request Oct 13, 2026
rc.1, rc.2 and main all reported 0.2.0rc1 with the same features although
the code differs. 0.3.0a1 plus shm_order_v1 (shared-memory `order`, PR #125,
already implemented) tell them apart.

- _version 0.3.0a1; FEATURES + PROTOCOL.md table and feature tests
- PROTOCOL.md: retention_prune is in rc.1; runs.build_record/record_json/
  done_status arrive in 0.3.0a1; big TIFFs are mapped from 0.3.0a1 (no id)
- OPERATIONS.md: pre-release pin rule, tag <-> PEP 440 name rule, rc.2 did
  not bump, no rc.3, Windows file-lock caveat; install snippets -> alpha.1
- CI compat job: newest tag sorted with versionsort.suffix (final beats
  its own pre-releases, alpha.1 beats rc.2)
- conformance.json version_order, frozen tools_0.3 schema fixture, compat matrix
- release sandbox: expected Tools pin is an argument (default: read from the
  app's post_install hook) instead of a constant
- docs/RELEASE_NOTES_0.3.0-alpha.1.md (draft for the GitHub pre-release)


Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN

Co-authored-by: Claude <noreply@anthropic.com>
guijacquemet added a commit that referenced this pull request Oct 13, 2026
…test hygiene, CI hardening (#129)

* Audit fixes before the alpha: say why a big TIFF was not mapped, ledger, docs, test hygiene

- convert._memmap_tiff: a TIFF of 8 MiB or more that is read the plain way logs one warning (file, reason) to the log and the worker's stderr
- worker._orphan_exit: a temp folder that cannot be removed is logged
- ledger rows for #124 and #125, real PR numbers instead of "this PR", pending section replaced by a note
- docs: tag install URLs, OPERATIONS contradictions, HOST_FEATURES rows, Playground rule, features rule exception
- tests: new tests and sabotage rows, dead retention guard removed, gc test asserts, test count not hard-coded

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN

* CI hardening: pin the third-party action by SHA, release_sandbox takes Fiji and QuPath refs as inputs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN

* Ledger row for the told TIFF fallback

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2SN8SuEakTdiWy3F2WUqN

---------

Co-authored-by: Claude <noreply@anthropic.com>
@guijacquemet
guijacquemet deleted the claude/worker-memmap-forder branch October 14, 2026 16:08
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.

2 participants