Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a5b56a8
group_by support
rjzamora Jul 15, 2026
55e8d45
Merge remote-tracking branch 'upstream/main' into ordered-actor-optim…
rjzamora Jul 15, 2026
b2bcfe6
cleanup
rjzamora Jul 15, 2026
f84a8a6
avoid adjust_ordering code path when local aggregation succeeds
rjzamora Jul 16, 2026
6a9d867
Merge remote-tracking branch 'upstream/main' into ordered-actor-groupby
rjzamora Jul 16, 2026
e7577f7
Merge remote-tracking branch 'upstream/main' into ordered-actor-groupby
rjzamora Jul 16, 2026
1aaaf91
fix testing
rjzamora Jul 16, 2026
a8d03ee
update coverage
rjzamora Jul 16, 2026
4bd3d24
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 17, 2026
f257a83
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 18, 2026
fd28e30
Merge remote-tracking branch 'upstream/main' into ordered-actor-groupby
rjzamora Jul 20, 2026
3d9b8bc
fix alignment check
rjzamora Jul 20, 2026
ad72d22
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 20, 2026
26c7995
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 21, 2026
591a2bc
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 21, 2026
4e98d38
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 21, 2026
51680a2
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 22, 2026
6bbf31c
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 27, 2026
8296d1c
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 28, 2026
78cadd6
Merge branch 'main' into ordered-actor-groupby
rjzamora Jul 30, 2026
10efc2b
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 10, 2026
ce2120b
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 10, 2026
780f107
Merge remote-tracking branch 'upstream/main' into ordered-actor-groupby
rjzamora Aug 12, 2026
6ff22ed
address code review
rjzamora Aug 12, 2026
4c82701
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 12, 2026
42b2402
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 13, 2026
8bbd51f
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 14, 2026
583d22a
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 14, 2026
e64054e
Merge branch 'main' into ordered-actor-groupby
rjzamora Aug 18, 2026
73fface
Merge remote-tracking branch 'upstream/main' into ordered-actor-groupby
rjzamora Aug 20, 2026
81b90bb
use suggestion
rjzamora Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import polars as pl

import pylibcudf as plc
from cudf_streaming.channel_metadata import Ordering
from cudf_streaming.partition_utils import (
packed_data_from_cudf_packed_columns,
unpack_and_concat,
Expand All @@ -29,7 +30,6 @@
from cudf_polars.utils.cuda_stream import stream_ordered_after

if TYPE_CHECKING:
from cudf_streaming.channel_metadata import Ordering
from rapidsmpf.communicator.communicator import Communicator
from rapidsmpf.memory.buffer_resource import BufferResource
from rapidsmpf.memory.packed_data import PackedData
Expand All @@ -44,6 +44,15 @@
_PartitionRange = tuple[int, int]


def get_strict_ordering(ordering: Ordering, br: BufferResource) -> Ordering:
"""Return an equivalent Ordering with strict boundaries."""
return Ordering(
ordering.keys,
ordering.get_boundaries(br),
strict_boundaries=True,
)
Comment on lines +47 to +53

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note: This could also belong to Ordering (e.g. Ordering.as_strict(br: BufferResource)).

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.

I think this should be implemented on Ordering:

diff --git a/python/cudf_streaming/cudf_streaming/channel_metadata.pyx b/python/cudf_streaming/cudf_streaming/channel_metadata.pyx
index 70d5ebfadc..99b9d83e95 100644
--- a/python/cudf_streaming/cudf_streaming/channel_metadata.pyx
+++ b/python/cudf_streaming/cudf_streaming/channel_metadata.pyx
@@ -150,6 +150,11 @@ cdef class Ordering:
         ret._handle = move(ordering)
         return ret
 
+    def as_strict(self) -> Ordering:
+        return Ordering.from_cpp(
+            cpp_Ordering(self._handle.keys, self._handle.boundaries, True)
+        )
+
     @property
     def keys(self) -> tuple:
         """Sort keys, one per sort column."""

and

diff --git a/python/cudf_streaming/cudf_streaming/channel_metadata.pxd b/python/cudf_streaming/cudf_streaming/channel_metadata.pxd
index f2037164a9..f59e8c8c8b 100644
--- a/python/cudf_streaming/cudf_streaming/channel_metadata.pxd
+++ b/python/cudf_streaming/cudf_streaming/channel_metadata.pxd
@@ -41,6 +41,9 @@ cdef extern from "<cudf_streaming/channel_metadata.hpp>" \
         cpp_Ordering(
             vector[cpp_OrderKey], unique_ptr[cpp_TableChunk], bool_t
         ) except +ex_handler
+        cpp_Ordering(
+            vector[cpp_OrderKey], shared_ptr[cpp_TableChunk], bool_t
+        ) except +ex_handler
         vector[cpp_OrderKey] keys
         shared_ptr[cpp_TableChunk] boundaries
         bool_t strict_boundaries

I think.



@dataclass(frozen=True)
class _RoutingPlan:
"""Static ownership and exchange plan for one ordering adjustment."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,10 @@ async def sort_actor(
partitioning = NormalizedPartitioning.from_keys(
metadata_in.partitioning, comm.nranks, keys=order_keys
)
if partitioning.is_strictly_sorted(order_keys):
if partitioning.is_ordered(
order_keys,
level="local" if metadata_in.duplicated else "flat",
):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if tracer is not None:
tracer.decision = "already_sorted"
await chunkwise_evaluate(
Expand Down
241 changes: 239 additions & 2 deletions python/cudf_polars/cudf_polars/streaming/actor_graph/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
from cudf_streaming.channel_metadata import (
ChannelMetadata,
HashScheme,
OrderKey,
OrderScheme,
Ordering,
Partitioning,
)
from cudf_streaming.table_chunk import TableChunk
from rapidsmpf.communicator.single import new_communicator as single_comm
Expand All @@ -22,6 +26,11 @@
from cudf_polars.dsl.expr import Col, NamedExpr
from cudf_polars.dsl.ir import IR, Distinct, GroupBy, Select
from cudf_polars.dsl.utils.naming import names_to_indices, unique_names
from cudf_polars.streaming.actor_graph.collectives.ordering import (
_partition_range,
adjust_ordering,
get_strict_ordering,
)
from cudf_polars.streaming.actor_graph.collectives.shuffle import ShuffleManager
from cudf_polars.streaming.actor_graph.dispatch import (
generate_ir_sub_network,
Expand All @@ -38,10 +47,12 @@
empty_table_chunk,
evaluate_batch,
evaluate_chunk,
gather_in_task_group,
maybe_remap_partitioning,
process_children,
recv_metadata,
send_metadata,
shutdown_channels_on_error,
shutdown_on_error,
)
from cudf_polars.streaming.groupby import _has_stable_sorted_agg, combine, decompose
Expand All @@ -55,6 +66,7 @@
from cudf_polars.dsl.ir import IRExecutionContext
from cudf_polars.streaming.actor_graph.dispatch import SubNetGenerator
from cudf_polars.streaming.actor_graph.tracing import ActorTracer
from cudf_polars.streaming.actor_graph.utils import PartitioningLevel
from cudf_polars.typing import Schema


Expand Down Expand Up @@ -445,6 +457,178 @@ async def _shuffle_reduce(
await ch_out.drain(context)


def _remap_ordering_keys(
ordering: Ordering,
column_indices: tuple[int, ...],
br: BufferResource,
) -> Ordering:
"""Return ``ordering`` with keys remapped to another schema."""
return Ordering(
[
OrderKey(index, key.order, key.null_order)
for key, index in zip(ordering.keys, column_indices, strict=True)
],
ordering.get_boundaries(br),
strict_boundaries=ordering.strict_boundaries,
)
Comment on lines +460 to +473

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.

Again, I think this should be Ordering.remap(new_order_keys).



def _groupby_output_metadata(
ir: GroupBy | Distinct,
decomposed: DecomposedGroupBy,
local_count: int,
partitioning: Partitioning,
duplicated: bool, # noqa: FBT001
*,
context: Context,
) -> ChannelMetadata:
"""Return groupby output metadata after final reduction/select."""
partitioning = maybe_remap_partitioning(
decomposed.reduction_ir,
partitioning,
child_ir=decomposed.reduction_ir,
context=context,
)
if decomposed.select_ir is not None:
partitioning = maybe_remap_partitioning(
decomposed.select_ir,
partitioning,
child_ir=decomposed.reduction_ir,
context=context,
)
else:
partitioning = maybe_remap_partitioning(
ir,
partitioning,
child_ir=ir.children[0],
context=context,
)
return ChannelMetadata(
local_count=local_count,
partitioning=partitioning,
duplicated=duplicated,
)


async def _send_locally_aggregated_chunks(
context: Context,
decomposed: DecomposedGroupBy,
ir_context: IRExecutionContext,
ch_out: Channel[TableChunk],
ch_in: Channel[TableChunk],
target_partition_size: int,
*,
aggregated: TableChunk,
input_drained: bool,
) -> None:
"""Send locally aggregated chunks, then drain the remaining input."""
seq_num = 0
while True:
await send_chunk(
context,
ch_out,
_enforce_schema(aggregated, decomposed.reduction_ir.schema, context.br()),
seq_num,
tracer=None,
)
seq_num += 1
del aggregated
if input_drained:
break
aggregated, input_drained, _ = await _local_aggregation(
context,
decomposed,
ir_context,
ch_in,
target_partition_size,
)
await ch_out.drain(context)
Comment thread
wence- marked this conversation as resolved.


async def _ordered_adjust_reduce(
context: Context,
comm: Communicator,
decomposed: DecomposedGroupBy,
ir_context: IRExecutionContext,
ch_out: Channel[TableChunk],
ch_in: Channel[TableChunk],
metadata_in: ChannelMetadata,
collective_id: int,
target_partition_size: int,
*,
aggregated: TableChunk,
input_drained: bool,
input_ordering: Ordering,
tracer: ActorTracer | None = None,
) -> None:
"""Adjust locally aggregated data to strict ordering boundaries."""
partial_input_ordering = _remap_ordering_keys(
input_ordering,
decomposed.shuffle_indices[: len(input_ordering.keys)],
context.br(),
)
partial_output_ordering = get_strict_ordering(partial_input_ordering, context.br())
ch_local = context.create_channel()
ch_adjusted = context.create_channel()
adjusted_metadata = _adjusted_ordering_metadata(
comm, metadata_in, partial_output_ordering
)
metadata_out = _groupby_output_metadata(
decomposed.ir,
decomposed,
adjusted_metadata.local_count,
adjusted_metadata.partitioning,
adjusted_metadata.duplicated,
context=context,
)
if tracer is not None:
tracer.decision = "adjust_ordering"

await send_metadata(ch_out, context, metadata_out)
if tracer is not None and metadata_out.duplicated:
tracer.set_duplicated()

async def reduce_adjusted_chunks() -> None:
extract_irs = [decomposed.reduction_ir] + (
[decomposed.select_ir] if decomposed.select_ir else []
)
while (msg := await ch_adjusted.recv(context)) is not None:
chunk = await evaluate_chunk(
context,
TableChunk.from_message(msg, br=context.br()),
*extract_irs,
ir_context=ir_context,
)
await send_chunk(context, ch_out, chunk, msg.sequence_number, tracer=tracer)
await ch_out.drain(context)
Comment thread
rjzamora marked this conversation as resolved.

async with shutdown_channels_on_error(context, ch_local, ch_adjusted):
await gather_in_task_group(
_send_locally_aggregated_chunks(
context,
decomposed,
ir_context,
ch_local,
ch_in,
target_partition_size,
aggregated=aggregated,
input_drained=input_drained,
),
adjust_ordering(
context,
comm,
decomposed.reduction_ir,
ir_context,
ch_adjusted,
ch_local,
partial_input_ordering,
partial_output_ordering,
collective_id=collective_id,
),
reduce_adjusted_chunks(),
)
Comment on lines +548 to +629

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Add a benchmark for the ordered reduction path.

Benchmark sorted, maintain-order groupby against the hash-shuffle path across several cardinalities and partition counts. This PR’s main benefit is avoiding shuffle without penalizing successful local/tree reduction, so that contract should be measured.

As per coding guidelines, Python feature contributions must add unit benchmarks.

🤖 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 `@python/cudf_polars/cudf_polars/streaming/actor_graph/groupby.py` around lines
547 - 630, Add a Python unit benchmark covering the ordered reduction path
centered on _ordered_adjust_reduce, comparing sorted maintain-order groupby with
the hash-shuffle implementation across multiple group cardinalities and
partition counts. Measure successful local/tree reduction performance and verify
the ordered path avoids shuffle, following the repository’s existing benchmark
conventions.

Source: Coding guidelines



def _enforce_schema(
chunk: TableChunk,
canonical_schema: dict[str, Any],
Expand Down Expand Up @@ -506,6 +690,27 @@ def _maintain_order(ir: GroupBy | Distinct) -> bool:
)


def _partition_count_for_rank(rank: int, nranks: int, npartitions: int) -> int:
"""Return the contiguous output-partition count owned by one rank."""
start, stop = _partition_range(rank, nranks, npartitions)
return stop - start

Comment thread
wence- marked this conversation as resolved.

def _adjusted_ordering_metadata(
comm: Communicator,
metadata_in: ChannelMetadata,
output_ordering: Ordering,
) -> ChannelMetadata:
"""Return metadata for data adjusted to strict ordering boundaries."""
return ChannelMetadata(
local_count=_partition_count_for_rank(
comm.rank, comm.nranks, output_ordering.num_boundaries + 1
),
partitioning=Partitioning(OrderScheme([output_ordering]), "inherit"),
duplicated=metadata_in.duplicated,
)


async def _choose_strategy(
context: Context,
comm: Communicator,
Expand Down Expand Up @@ -660,13 +865,19 @@ async def groupby_actor(
metadata_in = await recv_metadata(ch_in, context)

nranks = comm.nranks
group_keys = _key_indices(ir, ir.children[0].schema, concrete_prefix=True)
partitioning = NormalizedPartitioning.from_keys(
metadata_in.partitioning,
nranks,
keys=_key_indices(ir, ir.children[0].schema, concrete_prefix=True),
keys=group_keys,
)
partitioning_level: PartitioningLevel = (
"local" if metadata_in.duplicated else "flat"
)
maintain_order = _maintain_order(ir)
fully_partitioned = partitioning.is_strictly_partitioned()
fully_partitioned = partitioning.is_strictly_partitioned(
level=partitioning_level,
)
fallback_case = (
# NOTE: This criteria means that we fell back
# to one partition at lowering time.
Expand Down Expand Up @@ -741,6 +952,32 @@ async def groupby_actor(
aggregated=aggregated,
tracer=tracer,
)
elif (
# adjust_ordering requires row-ordered chunks. maintain_order=True
# preserves key order through local aggregation for ordered input.
maintain_order
and not metadata_in.duplicated
and partitioning.is_ordered(
group_keys,
level="flat",
)
):
assert isinstance(partitioning.inter_rank_scheme, OrderScheme)
await _ordered_adjust_reduce(
context,
comm,
decomposed,
ir_context,
ch_out,
ch_in,
metadata_in,
collective_ids.pop(),
target_partition_size,
aggregated=aggregated,
input_drained=input_drained,
input_ordering=partitioning.inter_rank_scheme.orderings[0],
tracer=tracer,
)
else:
await _shuffle_reduce(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,9 @@ async def over_actor(
keys=ir.key_indices,
allow_subset=True,
)
if partitioning.is_strictly_partitioned():
if partitioning.is_strictly_partitioned(
level="local" if metadata_in.duplicated else "flat",
):
metadata_out = ChannelMetadata(
local_count=metadata_in.local_count,
partitioning=maybe_remap_partitioning(
Expand Down
Loading
Loading