Skip to content

Parallelize the spill reducer (intra-block partitions + inter-block k-way) β€” blocked by #279Β #280

Description

@espg

πŸ€– from Claude

Summary

Follow-on to #279. Once the t-digest reduce loop is vectorized numpy (GIL-releasing) and the fold is a single order-independent k-way merge, the SpillAggregator reducer can be parallelized for another ~2–4Γ— on heavy shards. This is an engine-level change to src/zagg/processing/spill.py (the reducer thread + the memory budget), not a user-side agg function β€” at most gated by a worker knob (e.g. streaming.reduce_workers: N). Like #279 it ships in the packaged zagg, so fleet use needs a layer redeploy; it is fully testable locally without one.

Blocked by #279 β€” specifically:

Current model

_close_block β†’ _reduce_one on one threading.Thread; _join_reducer before the next close. So reads overlap one block's reduce, but blocks reduce strictly serially in close order, and _fold_block walks its partitions sequentially (spill.py:598-621).

Two flavors of parallelism

Flavor A β€” intra-block, across partitions (clean, no correctness change).
_fold_block loops for key in block.partition_keys(). Partitions are disjoint morton cell-groups β€” within a block a cell lives in exactly one partition β€” so folding partitions concurrently touches disjoint cells. No associativity/order issue. Each partition worker returns its own {cell: digest} and the disjoint results are dict.update-merged single-threaded (avoid mutating shared self._digests/self._counts from N threads). Parallelizes both the partition read-back I/O and the (post-#279) numpy CPU. Realistic degree = Lambda vCPUs (~2 near 1.8 GB, up to 6 at 10 GB).

Flavor B β€” inter-block, parallel block reduces (rides on #279 Phase 3).
The same cell recurs in every block, so folding two blocks concurrently into shared self._digests[cell] is racy and order-dependent. With #279's k-way merge, each block reduces independently to its own per-block-per-cell digest (no shared state), then a single k-way merge per cell combines across blocks at the end β€” parallel-safe and block-order-invariant.

Central design question β€” the memory budget

_default_block_bytes / _BUILD_MULT (spill.py:344-367) are derived assuming one partition reduced at a time, at ~3Γ— its spilled bytes, alongside the 25%-of-memory read buffer. P-way parallelism multiplies that peak working set by P β€” it would blow the Lambda memory ceiling unless the threshold is re-derived (shrink block_bytes by ~P, or gate the parallelism degree on measured free memory / vCPU count). This re-derivation + re-measuring the replay peaks (the #217 phase-3 slabs) is the substantive design work here, and it couples to the deployment memory/vCPU tier. This is why it is its own PR, not part of #279.

Proposed phasing (once unblocked)

  1. Flavor A β€” parallelize _fold_block across partitions; per-partition workers return disjoint {cell: digest}; merge single-threaded. Memory-budget re-derivation as the gating design decision (measure peak vs _BUILD_MULT Γ— P).
  2. Flavor B β€” parallel block reduces on top of t-digest agg hot path is unvectorized: ~15Γ— build + ~6Γ— fold available in pure numpyΒ #279's k-way merge; independent per-block digests + final k-way combine; assert block-order-invariance in tests.

Open questions

  1. Parallelism degree: fixed knob (reduce_workers), or auto from os.cpu_count() / memory budget? Recommendation: auto-derive, capped, with an override knob.
  2. Whether Flavor A alone (simplest, no t-digest agg hot path is unvectorized: ~15Γ— build + ~6Γ— fold available in pure numpyΒ #279 Phase 3 dependency beyond the vectorized loop) is enough on the real heavy-shard slabs before taking on Flavor B β€” decide after measuring Flavor A on the Chunk-scoped digest streaming: bound worker memory to one inner chunk (unlock coarse HEALPix orders)Β #217 replays.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions