Skip to content

Parallelize iceberg image crop export across worker processes - #495

Merged
d33bs merged 5 commits into
cytomining:mainfrom
d33bs:images-performance
Aug 10, 2026
Merged

d33bs merged 5 commits into
cytomining:mainfrom
d33bs:images-performance

Conversation

@d33bs

@d33bs d33bs commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

Description

Image export is much faster now. Crop work runs across multiple worker
processes automatically (~8 by default), cutting a ~17 min runs from ExampleHuman to a few
minutes. Tunable via image_crop_workers; small runs are unchanged.

What is the nature of your change?

  • Bug fix (fixes an issue).
  • Enhancement (adds functionality).
  • Breaking change (fix or feature that would cause existing functionality to not work as expected).
  • This change requires a documentation update.

Checklist

Please ensure that all boxes are checked before indicating that a pull request is ready for review.

  • I have read the CONTRIBUTING.md guidelines.
  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • My changes generate no new warnings.
  • New and existing unit tests pass locally with my changes.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have deleted all non-relevant text in this pull request template.

Summary by CodeRabbit

  • New Features

    • Image cropping and source-image exports now support configurable parallel processing.
    • Added the image_crop_workers option to conversion and Iceberg export workflows.
    • Automatic worker selection is available by default, while small workloads can run serially.
    • Output ordering, schema consistency, and source-image deduplication are preserved.
  • Documentation

    • Added guidance on performance tuning, worker settings, serial execution, process spawning, and avoiding CPU oversubscription.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3fa9b815-96d6-4c2c-a15f-598bbf1ff00a

📥 Commits

Reviewing files that changed from the base of the PR and between ab76172 and 60e9ce1.

📒 Files selected for processing (5)
  • cytotable/convert.py
  • cytotable/warehouse/iceberg.py
  • cytotable/warehouse/images.py
  • docs/source/tutorials/iceberg_warehouse_with_images.md
  • tests/test_iceberg.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • cytotable/convert.py
  • tests/test_iceberg.py
  • cytotable/warehouse/iceberg.py

📝 Walkthrough

Walkthrough

The PR adds image_crop_workers to the conversion and Iceberg warehouse APIs. Image crop and source-image exports support configurable spawned workers, temporary Parquet shards, ordered merging, compatible schemas, and cross-shard deduplication.

Changes

Iceberg image export parallelism

Layer / File(s) Summary
Worker configuration and warehouse wiring
cytotable/convert.py, cytotable/warehouse/iceberg.py, docs/source/overview.md, docs/source/tutorials/iceberg_warehouse_with_images.md, tests/test_convert.py
The public APIs accept image_crop_workers and forward it through asynchronous and sequential export paths. Documentation describes automatic, serial, capped, and spawned-worker behavior.
Parallel crop processing
cytotable/warehouse/images.py, tests/test_iceberg.py
Crop exports resolve worker counts, process temporary Parquet shards with spawned workers, preserve output order and schemas, and retain serial behavior for small workloads. Tests verify parity, determinism, key columns, and threshold behavior.
Parallel source-image processing
cytotable/warehouse/images.py, tests/test_iceberg.py
Source-image exports process shards with spawned workers, filter unresolved images, preserve empty-shard schemas, and deduplicate first occurrences across shards. Tests compare parallel and serial output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant convert
  participant write_iceberg_warehouse
  participant image_export
  participant ProcessPoolExecutor
  participant ArrowTables
  convert->>write_iceberg_warehouse: pass image_crop_workers
  write_iceberg_warehouse->>image_export: submit crop or source-image task
  image_export->>ProcessPoolExecutor: process shards when threshold is met
  ProcessPoolExecutor->>ArrowTables: build shard tables
  ArrowTables-->>image_export: merge ordered and deduplicated results
  image_export-->>write_iceberg_warehouse: return image export table
Loading

Possibly related PRs

  • cytomining/CytoTable#465: Overlaps in convert.py, write_iceberg_warehouse, and image-export handling, but adds different options and behaviors.

Suggested reviewers: gwaybio

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: process-based parallelization of Iceberg image crop exports.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@d33bs

d33bs commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cytotable/warehouse/images.py (1)

778-805: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Thicken empty shard outputs to the expected schema.

Both table builders build key_field_names from the received rows. A shard that skips all rows returns only fixed columns, while sibling shards include key columns, and _image_crop_table_parallel / _source_image_table_parallel concatenate without schema promotion.

Compute the expected rows from chunk data, then pass those key names into each shard worker so empty shards emit the same Arrow schema, or normalize the empty shard output before pa.concatenate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cytotable/warehouse/images.py` around lines 778 - 805, Update
_image_crop_table_parallel and _source_image_table_parallel to derive expected
key-field names from the full chunk data and pass them to _rows_to_crop_table
and the corresponding source-table builder, so shards with no rows still emit
the complete schema before concatenation. Apply this at
cytotable/warehouse/images.py:778-805 and
cytotable/warehouse/images.py:1176-1183; preserve existing fixed-column behavior
while ensuring empty shard outputs match non-empty siblings.
🧹 Nitpick comments (4)
cytotable/warehouse/images.py (1)

907-917: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Index rebuild cost is not cheap for cloud image directories.

Each shard worker rebuilds the image, mask, and outline indexes. _build_file_index calls cloud_glob(root, "**/*"), which performs a full recursive listing. With 8 shards this becomes 24 recursive listings per chunk instead of 3. On a local filesystem this is minor. On an s3://, gs://, or az:// prefix each listing is a paginated remote LIST, so the parallel path can add latency and request cost that partly offsets the crop speedup.

The docstring calls these "a few cheap dir walks", which does not hold for the cloud case. Consider building the index once in the parent and passing the resolved path strings to the workers, or restricting parallel mode to local image_dir values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cytotable/warehouse/images.py` around lines 907 - 917, Update the
multiprocessing path around the worker that calls _build_file_index so
cloud-backed image, mask, and outline indexes are built once in the parent and
passed to workers as resolved path strings, avoiding repeated recursive cloud
listings per shard. Preserve local behavior and ensure the worker reuses the
supplied indexes rather than rebuilding them.
cytotable/warehouse/iceberg.py (1)

896-925: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Note the multiplicative process count across chunks.

When the Parsl thread executor is present, this loop submits one task per chunk concurrently. Each task then creates its own ProcessPoolExecutor with up to image_crop_workers processes. The total process count is therefore the thread-pool width multiplied by image_crop_workers, which can oversubscribe cores and multiply peak memory on large plates.

The docstring at lines 657-659 already tells users to lower the value. Consider deriving a per-chunk budget from the thread-executor width instead, so the default stays safe without manual tuning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cytotable/warehouse/iceberg.py` around lines 896 - 925, Adjust the concurrent
chunk submission in the use_threads path so each _image_crop_table_app and
_source_image_table_app receives a per-chunk image_crop_workers budget derived
from the Parsl thread-executor width, rather than the full configured value.
Keep the total process usage bounded across concurrently submitted chunks while
preserving the existing behavior for non-threaded execution and the
include_source_images condition.
tests/test_iceberg.py (1)

1226-1260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for shards that produce no rows.

This test uses 30 rows with 2 workers, so _write_row_shards produces two evenly sized, fully populated shards. Every existing parallel test has the same shape. Two failure modes therefore stay uncovered:

  1. A shard whose input rows are all skipped, for example an unresolvable image filename or an invalid bounding box across a contiguous row block. That shard returns a table without key columns, and pa.concat_tables then sees mismatched schemas.
  2. A chunk with fewer rows than workers, which makes _write_row_shards emit empty trailing shards.

Add a case that mixes resolvable and unresolvable image filenames across the shard boundary, and a case with crop_workers greater than the row count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_iceberg.py` around lines 1226 - 1260, Extend
test_source_image_parallel_dedup_matches_serial with coverage for parallel
shards that yield no rows: include resolvable and unresolvable filenames
arranged across a shard boundary, and add a scenario where crop_workers exceeds
the chunk’s row count to create empty trailing shards. Compare serial and
parallel outputs and verify schemas, row counts, and deduplicated
Metadata_ImageID values remain consistent.
docs/source/overview.md (1)

181-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use source-level links instead of hard-coded HTML paths.

docs/source/tutorials/iceberg_warehouse_with_images.md is in the docs/source/tutorials/ tree, while overview.md is under docs/source/. The link should be tutorials/iceberg_warehouse_with_images.md for the Markdown/MyST source; keep the generated .html path separate or derive it with a Sphinx role.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/source/overview.md` at line 181, Update the image crops tutorial link in
the overview documentation to use the source-level relative path
tutorials/iceberg_warehouse_with_images.md instead of the hard-coded generated
HTML path, preserving the existing link text and surrounding guidance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cytotable/warehouse/images.py`:
- Around line 1305-1337: Update _source_image_table_parallel to shard work by
unique source_image_file values rather than rows, ensuring each distinct image
name is assigned to exactly one shard and all rows referencing it remain
together. Replace or adapt _write_row_shards usage accordingly, preserve the
existing worker execution and result merge behavior, and retain
_dedup_table_by_first_occurrence only as a safety net.
- Around line 942-953: Update _write_row_shards to clamp each computed end
offset to n before the start >= end check, ensuring no shard is created when
start is beyond the table’s row count. Preserve the existing shard sizing and
return behavior for non-empty ranges, including the final shard ending at n.
- Around line 983-984: Update both ProcessPoolExecutor constructions in the
shard-processing flows, including the one using worker and the corresponding
_crop_shard_worker/_source_shard_worker flow, to pass an explicit
multiprocessing context obtained with the "spawn" start method. Keep the
existing worker mapping and max_workers behavior unchanged.

---

Outside diff comments:
In `@cytotable/warehouse/images.py`:
- Around line 778-805: Update _image_crop_table_parallel and
_source_image_table_parallel to derive expected key-field names from the full
chunk data and pass them to _rows_to_crop_table and the corresponding
source-table builder, so shards with no rows still emit the complete schema
before concatenation. Apply this at cytotable/warehouse/images.py:778-805 and
cytotable/warehouse/images.py:1176-1183; preserve existing fixed-column behavior
while ensuring empty shard outputs match non-empty siblings.

---

Nitpick comments:
In `@cytotable/warehouse/iceberg.py`:
- Around line 896-925: Adjust the concurrent chunk submission in the use_threads
path so each _image_crop_table_app and _source_image_table_app receives a
per-chunk image_crop_workers budget derived from the Parsl thread-executor
width, rather than the full configured value. Keep the total process usage
bounded across concurrently submitted chunks while preserving the existing
behavior for non-threaded execution and the include_source_images condition.

In `@cytotable/warehouse/images.py`:
- Around line 907-917: Update the multiprocessing path around the worker that
calls _build_file_index so cloud-backed image, mask, and outline indexes are
built once in the parent and passed to workers as resolved path strings,
avoiding repeated recursive cloud listings per shard. Preserve local behavior
and ensure the worker reuses the supplied indexes rather than rebuilding them.

In `@docs/source/overview.md`:
- Line 181: Update the image crops tutorial link in the overview documentation
to use the source-level relative path tutorials/iceberg_warehouse_with_images.md
instead of the hard-coded generated HTML path, preserving the existing link text
and surrounding guidance.

In `@tests/test_iceberg.py`:
- Around line 1226-1260: Extend test_source_image_parallel_dedup_matches_serial
with coverage for parallel shards that yield no rows: include resolvable and
unresolvable filenames arranged across a shard boundary, and add a scenario
where crop_workers exceeds the chunk’s row count to create empty trailing
shards. Compare serial and parallel outputs and verify schemas, row counts, and
deduplicated Metadata_ImageID values remain consistent.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f0c2154-9a99-4791-9e56-f9741477fdc7

📥 Commits

Reviewing files that changed from the base of the PR and between bd901cb and 9e39555.

📒 Files selected for processing (6)
  • cytotable/convert.py
  • cytotable/warehouse/iceberg.py
  • cytotable/warehouse/images.py
  • docs/source/overview.md
  • docs/source/tutorials/iceberg_warehouse_with_images.md
  • tests/test_iceberg.py

Comment thread cytotable/warehouse/images.py
Comment thread cytotable/warehouse/images.py Outdated
Comment thread cytotable/warehouse/images.py
@d33bs
d33bs marked this pull request as ready for review August 6, 2026 04:38
@d33bs
d33bs requested a review from gwaybio as a code owner August 6, 2026 04:38
Comment thread tests/test_iceberg.py
Co-Authored-By: Gregory Way <gregory.way@gmail.com>

@jenna-tomkinson jenna-tomkinson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one general comment regarding the workers. Glad to see we found a bottleneck!

Comment thread cytotable/warehouse/images.py Outdated
Comment thread cytotable/warehouse/images.py
@jenna-tomkinson

Copy link
Copy Markdown
Member

@d33bs In your test, can you confirm if the number of cells in the warehouse joined_profiles matches the number that we expect from the SQLite (based on number of cytoplasm in the table)?

Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
@d33bs

d33bs commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Thanks @gwaybio and @jenna-tomkinson .

can you confirm

To my knowledge, it does match. The source of how I compared can be found here: https://github.com/d33bs/SK-N-AS_cytotable_pilot .

@jenna-tomkinson

Copy link
Copy Markdown
Member
image I confirmed from the SQLite that this looks like it is correct! I asked as we had an old bug in CT v1 where there was a mismatch so making sure this did not resurrect like a bug zombie of sort. Thanks, Dave!

@d33bs

d33bs commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Thanks @jenna-tomkinson and @gwaybio !

@d33bs
d33bs merged commit 5bf4ab6 into cytomining:main Aug 10, 2026
12 of 21 checks passed
@d33bs
d33bs deleted the images-performance branch August 10, 2026 14:01
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.

3 participants