Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8a3c7c
add locally_ordered attribute to Ordering
rjzamora Aug 26, 2026
fcceff8
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 26, 2026
bd8aabc
fix dangerous ordering propagation and upate adjust_ordering
rjzamora Aug 26, 2026
fca4fbb
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 26, 2026
df76309
cleanup
rjzamora Aug 26, 2026
b95f816
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 26, 2026
121240d
fix groupby maingain_order gate
rjzamora Aug 26, 2026
344cf23
address coderabbit comments
rjzamora Aug 26, 2026
9c29276
fix adjust_ordering using plc.merge.merge
rjzamora Aug 26, 2026
074ca51
address code-review comments
rjzamora Aug 27, 2026
0f855e8
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 27, 2026
41eb29b
revisions
rjzamora Aug 27, 2026
0c88e0f
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 27, 2026
4921ca4
remove _exprs_are_pointwise
rjzamora Aug 27, 2026
d6969b8
improve all_pointwise
rjzamora Aug 27, 2026
92a7860
add test
rjzamora Aug 27, 2026
ec9c5f0
drop unnecessary tests
rjzamora Aug 27, 2026
7cca6b3
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 29, 2026
0d8482d
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 31, 2026
da2c1b5
Merge remote-tracking branch 'upstream/main' into locally-ordered
rjzamora Aug 31, 2026
cbf2b27
Merge branch 'main' into locally-ordered
rjzamora Aug 31, 2026
2fd9581
Merge branch 'main' into locally-ordered
rjzamora Aug 31, 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
36 changes: 26 additions & 10 deletions cpp/libcudf_streaming/include/cudf_streaming/channel_metadata.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct order_key {
};

/**
* @brief A valid ordering description for sorted/range-partitioned data.
* @brief A valid ordering description for order-partitioned data.
*
* Data is partitioned by value ranges based on predetermined boundaries.
* For N partitions, there are N-1 boundary rows:
Expand All @@ -77,12 +77,18 @@ struct order_key {
* `strict_boundaries`: when true, every row in a chunk belongs to a single partition's
* half-open key range (partition keys do not straddle chunk interiors). When false,
* a chunk may contain keys spanning multiple partitions.
*
* `locally_ordered`: when true, rows within each partition at this level are ordered by
* `keys`. When false, partitions are still ordered across partition boundaries, but row
* order within a partition is not guaranteed.
*/
struct ordering {
std::vector<order_key> keys; ///< Sort keys (column, order, null_order per entry).
std::shared_ptr<table_chunk> boundaries; ///< N-1 boundary rows for N partitions.
/// See struct-level note on `strict_boundaries` semantics.
bool strict_boundaries{false};
/// See struct-level note on `locally_ordered` semantics.
bool locally_ordered{true};

/// @brief Default constructor. Produces an invalid (empty) ordering.
ordering() = default;
Expand All @@ -94,12 +100,14 @@ struct ordering {
* @param boundaries Non-null, device-resident boundary table (N-1 rows for N
* partitions). Accepts a `unique_ptr<table_chunk>` via implicit conversion.
* @param strict_boundaries See struct-level doc. Defaults to false.
* @param locally_ordered See struct-level doc. Defaults to true.
* @throws std::invalid_argument if `keys` is empty, `boundaries` is null or not
* device-resident, or `keys.size() != boundaries->shape().second`.
*/
ordering(std::vector<order_key> keys,
std::shared_ptr<table_chunk> boundaries,
bool strict_boundaries = false);
bool strict_boundaries = false,
bool locally_ordered = true);

/**
* @brief Return a new ordering with updated key column indices, sharing
Expand All @@ -108,32 +116,39 @@ struct ordering {
* @param new_keys Replacement sort keys; size must equal
* `boundaries->shape().second`.
* @return A new ordering with `new_keys` and the same boundaries and
* strictness.
* ordering flags.
* @throws std::invalid_argument if `new_keys` is empty or size mismatches
* boundaries.
*/
[[nodiscard]] ordering with_keys(std::vector<order_key> new_keys) const;

/**
* @brief Return a new ordering with updated local row-order metadata.
*
* @param locally_ordered Whether rows within each partition are ordered by `keys`.
* @return A new ordering with the same keys, boundaries, and strictness.
*/
[[nodiscard]] ordering with_locally_ordered(bool locally_ordered) const;

/**
* @brief Check whether boundary values are aligned with another ordering.
*
* @param other The ordering to compare against.
* @param br Buffer resource used for temporary allocations during comparison.
* @return True when both orderings have matching boundary values and
* strict_boundaries attributes, and are otherwise compatible (same order and
* null_order).
* null_order). This comparison intentionally ignores `locally_ordered`.
*/
[[nodiscard]] bool boundaries_aligned_with(ordering const& other,
rapidsmpf::BufferResource& br) const;
};

/**
* @brief Order-based partitioning scheme for sorted/range-partitioned data.
* @brief Order-based partitioning scheme for order-partitioned data.
*
* An order_scheme advertises that the same stream is sorted/range-partitioned
* with respect to any individual ordering it contains. Consumers are
* responsible for selecting the ordering that is relevant to a particular
* operation.
* An order_scheme advertises that the same stream is order-partitioned with
* respect to any individual ordering it contains. Consumers are responsible for
* selecting the ordering that is relevant to a particular operation.
*/
struct order_scheme {
std::vector<ordering> orderings; ///< Ordering descriptions valid for the stream.
Expand All @@ -148,7 +163,8 @@ struct order_scheme {
*/
order_scheme(std::vector<order_key> keys,
std::shared_ptr<table_chunk> boundaries,
bool strict_boundaries = false);
bool strict_boundaries = false,
bool locally_ordered = true);

/**
* @brief Construct a validated multi-ordering order_scheme.
Expand Down
22 changes: 16 additions & 6 deletions cpp/libcudf_streaming/src/channel_metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,24 @@ void validate_ordering(ordering const& ordering)

ordering::ordering(std::vector<order_key> keys,
std::shared_ptr<table_chunk> boundaries,
bool strict_boundaries)
: keys{std::move(keys)}, boundaries{std::move(boundaries)}, strict_boundaries{strict_boundaries}
bool strict_boundaries,
bool locally_ordered)
: keys{std::move(keys)},
boundaries{std::move(boundaries)},
strict_boundaries{strict_boundaries},
locally_ordered{locally_ordered}
{
validate_ordering(*this);
}

ordering ordering::with_keys(std::vector<order_key> new_keys) const
{
return ordering{std::move(new_keys), boundaries, strict_boundaries};
return ordering{std::move(new_keys), boundaries, strict_boundaries, locally_ordered};
}

ordering ordering::with_locally_ordered(bool locally_ordered) const
{
return ordering{keys, boundaries, strict_boundaries, locally_ordered};
}

bool ordering::boundaries_aligned_with(ordering const& other, rapidsmpf::BufferResource& br) const
Expand Down Expand Up @@ -90,9 +99,10 @@ bool ordering::boundaries_aligned_with(ordering const& other, rapidsmpf::BufferR

order_scheme::order_scheme(std::vector<order_key> keys,
std::shared_ptr<table_chunk> boundaries,
bool strict_boundaries)
: order_scheme(
std::vector<ordering>{ordering{std::move(keys), std::move(boundaries), strict_boundaries}})
bool strict_boundaries,
bool locally_ordered)
: order_scheme(std::vector<ordering>{
ordering{std::move(keys), std::move(boundaries), strict_boundaries, locally_ordered}})
{
}

Expand Down
22 changes: 22 additions & 0 deletions cpp/libcudf_streaming/tests/streaming/test_channel_metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,28 @@ TEST_F(StreamingChannelMetadataGPU, OrderingReplaceKeys)
EXPECT_EQ(o2.keys[0].column_index, 5);
EXPECT_EQ(o2.keys[0].order, cudf::order::DESCENDING);
EXPECT_EQ(o2.strict_boundaries, o1.strict_boundaries);
EXPECT_EQ(o2.locally_ordered, o1.locally_ordered);
EXPECT_EQ(o2.boundaries->shape(), o1.boundaries->shape());
EXPECT_EQ(o2.boundaries.get(), b.get());
EXPECT_NE(o1.keys[0].column_index, o2.keys[0].column_index);

EXPECT_THROW(static_cast<void>(o1.with_keys({k0, k5})), std::invalid_argument);
}

TEST_F(StreamingChannelMetadataGPU, OrderingReplaceLocallyOrdered)
{
order_key k0{0, cudf::order::ASCENDING, cudf::null_order::BEFORE};

auto b = make_chunk({100, 200});
ordering o1 = ordering({k0}, b, /*strict_boundaries=*/true);
auto o2 = o1.with_locally_ordered(false);

EXPECT_EQ(o2.keys[0], o1.keys[0]);
EXPECT_EQ(o2.strict_boundaries, o1.strict_boundaries);
EXPECT_FALSE(o2.locally_ordered);
EXPECT_EQ(o2.boundaries.get(), b.get());
}

TEST_F(StreamingChannelMetadataGPU, OrderSchemeMultipleOrderings)
{
order_key k0{0, cudf::order::ASCENDING, cudf::null_order::BEFORE};
Expand All @@ -212,6 +227,7 @@ TEST_F(StreamingChannelMetadataGPU, OrderSchemeMultipleOrderings)
EXPECT_EQ(o.orderings[0].keys[0], k0);
EXPECT_EQ(o.orderings[0].boundaries.get(), b0.get());
EXPECT_TRUE(o.orderings[0].strict_boundaries);
EXPECT_TRUE(o.orderings[0].locally_ordered);
EXPECT_EQ(o.orderings[1].keys[0], k2);
EXPECT_EQ(o.orderings[1].boundaries.get(), b1.get());
EXPECT_FALSE(o.orderings[1].strict_boundaries);
Expand All @@ -232,6 +248,12 @@ TEST_F(StreamingChannelMetadataGPU, OrderingBoundariesAlignedWith)
ordering o_strict({k0}, make_chunk({100, 200}), /*strict_boundaries=*/true);
EXPECT_FALSE(o1.boundaries_aligned_with(o_strict, *br));

ordering o_unordered({k0},
make_chunk({100, 200}),
/*strict_boundaries=*/false,
/*locally_ordered=*/false);
EXPECT_TRUE(o1.boundaries_aligned_with(o_unordered, *br));

ordering o_diff({k0}, make_chunk({100, 300}));
EXPECT_FALSE(o1.boundaries_aligned_with(o_diff, *br));
}
Expand Down
11 changes: 10 additions & 1 deletion python/cudf_polars/cudf_polars/dsl/expressions/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# TODO: remove need for this
# ruff: noqa: D101
Expand All @@ -14,6 +14,7 @@

from cudf_polars.containers import Column
from cudf_polars.dsl.nodebase import Node
from cudf_polars.dsl.traversal import traversal

if TYPE_CHECKING:
from typing import Self
Expand Down Expand Up @@ -109,6 +110,10 @@ def evaluate(
"""
return self.do_evaluate(df, context=context)

def all_pointwise(self) -> bool:
"""Return True when this expression and all descendants are pointwise."""
return all(e.is_pointwise for e in traversal([self]))

@property
def agg_request(self) -> plc.aggregation.Aggregation:
"""
Expand Down Expand Up @@ -199,6 +204,10 @@ def evaluate(
"""
return self.value.evaluate(df, context=context).rename(self.name)

def all_pointwise(self) -> bool:
"""Return True when the underlying expression tree is pointwise."""
return self.value.all_pointwise()

def reconstruct(self, expr: Expr) -> Self:
"""
Rebuild with a new `Expr` value.
Expand Down
43 changes: 43 additions & 0 deletions python/cudf_polars/cudf_polars/dsl/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,22 @@ class IR(Node["IR"]):
_non_child_args: tuple[Any, ...]
# The number of non-child arguments to pass to do_evaluate.
_n_non_child_args: ClassVar[int]
# Class-level opt-in for :attr:`preserves_output_order`.
_preserves_output_order: ClassVar[bool] = False
Comment on lines +253 to +254

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.

Do we need class variable? i.e. do we ever check this attribute on a class rather than an instance? Or can we remove this and define

@property
def preserves_output_order(self) -> bool:
    return False

I think that'll make it a bit clearer that this is (sometimes, depending on the IR type) a value-dependent thing.

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.

We never read it on the class directly, but it lets Filter/Slice/Projection/Cache/Rolling opt in with one line (_preserves_output_order: ClassVar[bool] = True). Only the "tricky" classes need to override the public preserves_output_order property.

We can absolutely drop this class variable, but then we would need to add the same property to those 5 classes (~20 extra lines of code). This is okay with me. I just want to make sure the purpose of _preserves_output_order was clear before I make the change.

schema: Schema
"""Mapping from column names to their data types."""

@property
def preserves_output_order(self) -> bool:
"""
Whether output rows appear in the same relative order as this node's input.

Only meaningful for nodes with a single input. Multi-input nodes
(``Join``, ``Union``) need per-child reasoning and are handled by
their streaming actors.
"""
return self._preserves_output_order

def get_hashable(self) -> Hashable:
"""
Hashable representation of node, treating schema dictionary.
Expand Down Expand Up @@ -1560,6 +1573,7 @@ class Cache(IR):
Used for CSE at the plan level.
"""

_preserves_output_order: ClassVar[bool] = True
__slots__ = ("key", "refcount")
_non_child = ("schema", "key", "refcount")
_n_non_child_args = 2
Expand Down Expand Up @@ -1784,6 +1798,11 @@ def __init__(
): # pragma: no cover
raise NotImplementedError(f"Unsupported scan type: {df.typ}")

@property
def preserves_output_order(self) -> bool:
"""Whether the selected expressions keep input appearance order."""
return all(e.all_pointwise() for e in self.exprs)

@staticmethod
def _is_len_expr(exprs: tuple[expr.NamedExpr, ...]) -> bool: # pragma: no cover
if len(exprs) == 1:
Expand Down Expand Up @@ -1913,6 +1932,7 @@ def do_evaluate(
class Rolling(IR):
"""Perform a (possibly grouped) rolling aggregation."""

_preserves_output_order: ClassVar[bool] = True
__slots__ = (
"agg_requests",
"closed_window",
Expand Down Expand Up @@ -2161,6 +2181,11 @@ def __init__(
self.zlice,
)

@property
def preserves_output_order(self) -> bool:
"""Whether grouped rows keep input appearance order."""
return self.maintain_order

@classmethod
@log_do_evaluate
@nvtx_annotate_cudf_polars(message="GroupBy")
Expand Down Expand Up @@ -3158,6 +3183,11 @@ def __init__(
self._non_child_args = (self.columns, self.should_broadcast)
self.children = (df,)

@property
def preserves_output_order(self) -> bool:
"""Whether the stacked expressions keep input appearance order."""
return all(e.all_pointwise() for e in self.columns)

@classmethod
@log_do_evaluate
@nvtx_annotate_cudf_polars(message="HStack")
Expand Down Expand Up @@ -3222,6 +3252,11 @@ def __init__(
self._non_child_args = (keep, subset, zlice, stable)
self.children = (df,)

@property
def preserves_output_order(self) -> bool:
"""Whether distinct rows keep input appearance order."""
return self.stable

_KEEP_MAP: ClassVar[dict[str, plc.stream_compaction.DuplicateKeepOption]] = {
"first": plc.stream_compaction.DuplicateKeepOption.KEEP_FIRST,
"last": plc.stream_compaction.DuplicateKeepOption.KEEP_LAST,
Expand Down Expand Up @@ -3370,6 +3405,7 @@ def do_evaluate(
class Slice(IR):
"""Slice a dataframe."""

_preserves_output_order: ClassVar[bool] = True
__slots__ = ("length", "offset")
_non_child = ("schema", "offset", "length")
_n_non_child_args = 2
Expand Down Expand Up @@ -3398,6 +3434,7 @@ def do_evaluate(
class Filter(IR):
"""Filter a dataframe with a boolean mask."""

_preserves_output_order: ClassVar[bool] = True
__slots__ = ("mask",)
_non_child = ("schema", "mask")
_n_non_child_args = 1
Expand All @@ -3423,6 +3460,7 @@ def do_evaluate(
class Projection(IR):
"""Select a subset of columns from a dataframe."""

_preserves_output_order: ClassVar[bool] = True
__slots__ = ()
_non_child = ("schema",)
_n_non_child_args = 1
Expand Down Expand Up @@ -3611,6 +3649,11 @@ def __init__(self, schema: Schema, name: str, options: Any, df: IR):
)
self._non_child_args = (schema, name, self.options)

@property
def preserves_output_order(self) -> bool:
"""Whether this map keeps input appearance order."""
return self.name in {"rechunk", "rename", "row_index", "hint_sorted"}

def get_hashable(self) -> Hashable:
"""
Hashable representation of the node.
Expand Down
Loading
Loading