Skip to content

Commit b8bacd5

Browse files
committed
refactor(ray): unify distributed partitioning on a shared vocabulary
Collapse the three distributed entry points into a single `execute_on_ray(partitions=...)` and introduce a backend-neutral partition vocabulary in `io_sources/partitions.py`. A partition is a `ReadPartition(predicate, key)`; `RayPartition` subclasses it to add per-task `remote_options`. Partitions come from builders: - `by_time(column, every)` — calendar windows from the pushed-down date range - `by_value(column, values)` — discrete values (derived from an IN filter when omitted) - `by_range(column, every)` — fixed-width numeric buckets - `by_key(partitions, by)` — equality on caller-enumerated keys (was execute_on_ray_by) - `discrete_partitions` / `cartesian_partitions` — member lists and date x bucket products `execute_on_ray` accepts a partitioner or an explicit ReadPartition list; the former requires a bounded pushed-down predicate. The legacy `date_column`/`time_unit` calendar call is kept as a shortcut. `execute_on_ray_by` and `execute_on_ray_partitions` are removed (the first becomes `by_key`, the second is just an explicit list). The vocabulary is intentionally backend-neutral so a SQL reader can consume the same partitions. Update the ray tests to the unified entry point and add parity tests for the builders; refresh the README and API-Reference. Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com>
1 parent decbd28 commit b8bacd5

6 files changed

Lines changed: 619 additions & 310 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ For a guided walkthrough, start with the
9090
- **Caching**`cache` keeps an in-memory, column- and partition-level cache for
9191
iterative work; `cache_parquet` materializes date-partitioned Parquet on local disk
9292
or S3, fetching only the partitions a query needs.
93-
- **Distributed execution**`execute_on_ray` splits a `LazyFrame` by calendar period
94-
across an existing Ray cluster; `execute_on_ray_by` / `execute_on_ray_partitions`
95-
partition by an arbitrary key or explicit specs. (For multi-stage distributed pipelines,
93+
- **Distributed execution**`execute_on_ray` splits a `LazyFrame` across an existing Ray
94+
cluster, one task per partition. Build partitions with `by_time` (calendar windows),
95+
`by_value` (discrete keys), `by_range` (numeric buckets), `by_key` (enumerated keys), or an
96+
explicit `ReadPartition` list. (For multi-stage distributed pipelines,
9697
[Polars Cloud](https://docs.cloud.pola.rs/polars-cloud/) is the more strategic option.)
9798
- **Ergonomics**`iter_rows` for memory-efficient row iteration, `debug` to inspect
9899
what Polars pushes into a source, and `disable_optimizations` to compare against plain

docs/wiki/API-Reference.md

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -110,40 +110,32 @@ common sub-plan elimination.
110110
### `execute_on_ray`
111111

112112
```python
113-
lf.piot.execute_on_ray(*, date_column, time_unit, return_as="arrow",
114-
remote_options=None, max_concurrency=100)
113+
lf.piot.execute_on_ray(partitions, *, return_as="arrow",
114+
remote_options=None, max_concurrency=100,
115+
preserve_partition_order=None)
115116
```
116117

117-
Split the LazyFrame into calendar periods and execute each on an already-initialised Ray
118-
cluster. `time_unit` is `"daily"`, `"monthly"`, or `"yearly"`. Requires `ray.init()` to
119-
have been called and a bounded predicate on `date_column`.
120-
121-
### `execute_on_ray_by`
122-
123-
```python
124-
lf.piot.execute_on_ray_by(partitions, by, *, remote_options=None,
125-
partition_remote_options=None, return_as="arrow",
126-
max_concurrency=100, preserve_partition_order=False)
127-
```
128-
129-
Distribute the LazyFrame across Ray by equality on caller-enumerated partition keys.
130-
`partitions` is a small frame (or `Series`/sequence) with one row per partition; `by` is a
131-
column name, list of names, selector, or a `pl.Expr` (e.g. `pl.col("id").hash() % N`). Each
132-
row runs one Ray task filtered to `col == key`. `partition_remote_options` sets per-partition
133-
Ray options from a struct column in `partitions` or a `{key: dict}` mapping.
134-
135-
### `execute_on_ray_partitions`
136-
137-
```python
138-
lf.piot.execute_on_ray_partitions(specs, *, remote_options=None, return_as="arrow",
139-
max_concurrency=100, preserve_partition_order=False)
140-
```
141-
142-
Distribute the LazyFrame using an explicit list of `RayPartition(predicate, key, remote_options)` specs — the general primitive for ranges, discrete member lists
143-
(`col.is_in(...)`), and compound predicates. Build common spec sets with `discrete_partitions`
144-
and `cartesian_partitions`.
145-
146-
Chaining multiple `execute_on_ray*` calls relies on predicate pushdown surviving intervening
118+
Distribute the LazyFrame across an already-initialised Ray cluster, running one task per
119+
partition. `partitions` is either a partitioner or an explicit iterable of
120+
`ReadPartition(predicate, key)` (a `RayPartition` additionally carries per-task
121+
`remote_options`). Build partitions with:
122+
123+
- `by_time(column, every)` — calendar windows derived from the pushed-down date range
124+
(`every` is `"1mo"`/`"2w"`/`"5d"`/`"1q"`/`"1y"` or an integer number of days).
125+
- `by_value(column, values=None)` — one task per discrete value; derived from the pushed-down
126+
`IN` filter when `values` is omitted.
127+
- `by_range(column, every)` — fixed-width numeric buckets over the pushed-down range.
128+
- `by_key(partitions, by, *, partition_remote_options=None)` — equality on caller-enumerated
129+
keys; `by` is a column name, list, selector, or a `pl.Expr` (e.g. `pl.col("id").hash() % N`).
130+
`partition_remote_options` sets per-partition Ray options from a struct column or `{key: dict}`.
131+
- `discrete_partitions` / `cartesian_partitions` — explicit `col.is_in(...)` member lists and
132+
`date_window × bucket` products.
133+
134+
A partitioner requires a bounded predicate on its column. Requires `ray.init()` to have been
135+
called. As a legacy shortcut, `execute_on_ray(date_column=..., time_unit="daily"|"monthly"|"yearly")`
136+
is equivalent to `partitions=by_time(date_column, ...)`.
137+
138+
Chaining multiple `execute_on_ray` calls relies on predicate pushdown surviving intervening
147139
operations — partition once at the outermost boundary. For multi-stage distributed pipelines,
148140
prefer [Polars Cloud](https://docs.cloud.pola.rs/polars-cloud/).
149141

polars_io_tools/io_sources/__init__.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .lazy_iter_rows import *
1919
from .lazy_narwhals_reader import *
2020
from .lazy_sql_reader import *
21+
from .partitions import *
2122
from .pushdown_combine import *
2223
from .pushdown_pivot import *
2324
from .pushdown_unpivot import *
@@ -37,6 +38,7 @@
3738
from .lazy_clickhouse_writer import sink_clickhouse # noqa: TC004
3839
from .lazy_data_generator import scan_synthetic_panel, scan_synthetic_regression
3940
from .lazy_iter_rows import iter_rows # noqa: TC004
41+
from .partitions import KeyPartitions, ReadPartition, by_key, by_range, by_time, by_value
4042
from .pushdown_combine import FilterSpec, pushdown_combine
4143
from .pushdown_pivot import pushdown_pivot
4244
from .pushdown_unpivot import pushdown_unpivot
@@ -50,17 +52,11 @@
5052
# for the `functools.wraps` decorator to work. That's why we use a stub here.
5153
from .lazy_ray import (
5254
execute_on_ray as _execute_on_ray_proto,
53-
execute_on_ray_by as _execute_on_ray_by_proto,
54-
execute_on_ray_partitions as _execute_on_ray_partitions_proto,
5555
)
5656
else:
5757

5858
def _execute_on_ray_proto(*_a, **_kw): ...
5959

60-
def _execute_on_ray_by_proto(*_a, **_kw): ...
61-
62-
def _execute_on_ray_partitions_proto(*_a, **_kw): ...
63-
6460

6561
@pl.api.register_lazyframe_namespace("piot")
6662
class PIOTOperations:
@@ -99,20 +95,6 @@ def execute_on_ray(self, *args, **kwargs) -> pl.LazyFrame:
9995

10096
return _execute_on_ray(self._lf, *args, **kwargs)
10197

102-
@functools.wraps(_execute_on_ray_by_proto)
103-
def execute_on_ray_by(self, *args, **kwargs) -> pl.LazyFrame:
104-
# heavy import happens only when the user calls the method
105-
from .lazy_ray import execute_on_ray_by as _execute_on_ray_by
106-
107-
return _execute_on_ray_by(self._lf, *args, **kwargs)
108-
109-
@functools.wraps(_execute_on_ray_partitions_proto)
110-
def execute_on_ray_partitions(self, *args, **kwargs) -> pl.LazyFrame:
111-
# heavy import happens only when the user calls the method
112-
from .lazy_ray import execute_on_ray_partitions as _execute_on_ray_partitions
113-
114-
return _execute_on_ray_partitions(self._lf, *args, **kwargs)
115-
11698
@functools.wraps(filtered_join_asof)
11799
def filtered_join_asof(self, *args, **kwargs) -> pl.LazyFrame:
118100
if self._DISABLE_OPTIMIZATIONS:

0 commit comments

Comments
 (0)