Skip to content

Generalize merge-mode streaming toward the full reducer surface (parity with spill) #283

Description

@espg

🤖 from Claude

Motivation

The #280 reducer-coverage discussion surfaced an asymmetry: spill applies to all reducers (single-block is byte-identical to pooled, so even non-mergeable reducers work; only multi-block spill is mergeable-only), whereas merge-mode streaming is mergeable-only up frontvalidate_streaming rejects expressions, non-len scalars, and non-tdigest ragged before any reads. Today the only wired merge laws are len/count (summation) and the two tdigest reducers (#279).

This issue is to decide how (and how far) to generalize merge-mode toward the full reducer surface, so a config isn't silently forced onto spill/pooled just because it uses a common reduction.

The math — three tiers of reducer

  1. Trivially mergeable (associative), not yet wired. These have exact incremental laws and just need wiring:
    • min, max (elementwise), sum, count
    • mean (as running sum + count)
    • var / std (via streaming moments — Welford M2 / Chan parallel-variance combine)
    • bounding box / extent, any linear combination of the above
  2. Approximately mergeable. Exact form is non-mergeable but a mergeable sketch exists:
  3. Genuinely non-mergeable. No incremental law over flushes:
    • arbitrary expression: fields whose intermediate isn't a function of mergeable aggregates (e.g. np.median(h) directly, order-dependent ops)
    • histograms with data-dependent bins

Options

  • (a) Wire the tier-1 associative laws (min/max/sum/mean/moments) into the streaming/spill fold. Biggest coverage win for least risk — these are exact. Mirrors how len and tdigest are already wired.
  • (b) Policy for tier-3 (non-mergeable) fields. Choose one:
    • reject (status quo) — clear but forces manual backend choice.
    • auto-fallback — a config with any non-mergeable field auto-routes to spill (single-block exact) or pooled, instead of erroring. Best "just works" UX; needs care that the fallback's memory is acceptable.
    • partial-stream — fold the mergeable fields, buffer only the non-mergeable fields' raw obs. Hybrid; the non-mergeable fields lose the memory bound, so probably only sensible when they're few/small.
  • (c) Expression decomposition (stretch). Detect when an expression: is a pure function of mergeable aggregates (e.g. mean = sum/count) and stream it by folding the parts. Powerful but fiddly; likely a later phase.

Open questions

  1. Is the goal true parity with spill (accept every config, fall back as needed), or just widening the mergeable set (tier 1) and keeping an explicit reject for tier 3?
  2. For tier-3 fallback — auto-route to spill, auto-route to pooled, or keep rejecting with a clearer "use mode: spill / drop streaming" message?
  3. Should the associative laws live in one place shared by merge-mode and spill (they both fold), so a reducer's merge law is declared once? (Today len and tdigest are special-cased in both StreamingAggregator and SpillAggregator.)

Related: #279 (tdigest merge laws, the current template for wiring one), #280 (parallel spill reducer — Flavor A is reducer-agnostic, so it composes with whatever laws this issue adds). Labeled discuss — no code until the policy (esp. the tier-3 fallback) is settled.

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