Source code for pyphi.parallel.chunking
"""Pure index-partition helpers for parallel chunking.
No PyPhi imports: these decide how item indices are grouped into chunks,
either evenly (count-balanced) or by estimated cost (weight-balanced).
"""
from __future__ import annotations
import heapq
import math
from collections.abc import Callable
from collections.abc import Iterator
from collections.abc import Sequence
from typing import Any
_EPS = 1e-12
[docs]
def even_partition(n: int, k: int) -> list[list[int]]:
"""Split ``range(n)`` into ``min(k, n)`` contiguous, near-equal bins."""
k = max(1, min(k, n))
base, extra = divmod(n, k)
bins: list[list[int]] = []
start = 0
for i in range(k):
size = base + (1 if i < extra else 0)
bins.append(list(range(start, start + size)))
start += size
return bins
[docs]
def cost_balanced_partition(weights: list[float], k: int) -> list[list[int]]:
"""Greedily LPT-pack item indices into ``min(k, n)`` cost-balanced bins.
Sorts indices by weight descending and assigns each to the currently
lightest bin. Non-positive / non-finite weights are clamped to a small
epsilon so every item still lands in exactly one bin.
"""
n = len(weights)
k = max(1, min(k, n))
bins: list[list[int]] = [[] for _ in range(k)]
heap = [(0.0, i) for i in range(k)] # (accumulated weight, bin index)
order = sorted(range(n), key=lambda i: weights[i], reverse=True)
for idx in order:
w = weights[idx]
if not math.isfinite(w) or w <= 0.0:
w = _EPS
acc, b = heapq.heappop(heap)
bins[b].append(idx)
heapq.heappush(heap, (acc + w, b))
return bins
[docs]
def iter_chunks(
materialized: Sequence[Sequence[Any]],
chunksize: int,
num_workers: int,
size_func: Callable[[Any], float] | None = None,
) -> Iterator[tuple]:
"""Yield chunk tuples for parallel dispatch.
Each yielded tuple holds one list per input sequence, index-aligned
across sequences. Indices are grouped into ``max(ceil(n / chunksize),
num_workers)`` bins — evenly, or cost-balanced when ``size_func``
estimates per-item cost from the first sequence's items. ``n`` is the
length of the shortest sequence, so ragged inputs truncate as
``zip(strict=False)`` would.
Parameters
----------
materialized : sequence of sequences
The item sequences to chunk; the first is the primary axis.
chunksize : int
Target number of items per chunk.
num_workers : int
Lower bound on the number of chunks, so a small workload still
spreads across available workers.
size_func : callable, optional
Estimated cost of one primary-axis item. If None, chunks are
count-balanced.
"""
if not materialized or not materialized[0]:
return
n = min(len(it) for it in materialized)
k = max(math.ceil(n / chunksize), num_workers)
if size_func is not None:
weights = [size_func(materialized[0][i]) for i in range(n)]
index_bins = cost_balanced_partition(weights, k)
else:
index_bins = even_partition(n, k)
for indices in index_bins:
if not indices:
continue
yield tuple([it[i] for i in indices] for it in materialized)