Skip to content

Commit c2443da

Browse files
lmeyerovclaude
andcommitted
fix(gfql): the fused grouped-aggregate lane keeps the group_by plan on the GPU target (#2064)
The low-cardinality count(*) alternative lowers to value_counts + unnest, and cudf-polars has no unnest map function, so on engine='polars-gpu' every fused count(*) shape raised inside the lane and declined to the generic route. On the GPU target the lane now keeps the group_by formulation, which cudf-polars executes. Pin: the alternative declines under the GPU target and still produces the UNNEST plan on CPU. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QztW7jYsDd66e8rb8pJNQA (cherry picked from commit e1eb737)
1 parent 3ef1e70 commit c2443da

3 files changed

Lines changed: 26 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
2525
* GFQL: a multi-hop step (`hops=2`, `to_fixed_point`) whose edge alias shares its name with the column its own filter uses raised `incompatible-column-type` on pandas and cuDF; the backward re-execution now reads the graph's edge columns for that step, so the eager engines serve it like the single-hop form and return the same rows as polars (#2049).
2626
* GFQL: one alias-shadowing contract on every engine for op-list chains: an alias named like a column of the frame it marks becomes the boolean marker under that name (pandas, cuDF and now polars, which used to keep the user values and leak a `_right` join suffix). The shadowed values ride under the internal restore column the Cypher row pipeline already resolves, so Cypher keeps reading the user value through the variable (`MATCH (a)-[type:K]->(b) RETURN type.type`) on both engines; the polars residual decline for that shape is gone.
2727
* GFQL polars: an unnamed, untyped single-hop chain kept duplicate node rows (a node table carrying the same id twice) where pandas, cuDF and every other polars shape collapse them; the plain single-hop branch now returns one row per node id (#2051).
28+
* GFQL polars-gpu: the fused single-hop grouped-aggregate lane no longer takes the `value_counts` formulation on the GPU target; its `unnest` node has no cudf-polars implementation, so every fused `count(*)` shape declined to the generic route on `engine='polars-gpu'`. The `group_by` formulation is GPU-executable and answers the same rows (#2064).
29+
2830
- **cuDF 26.2 compatibility: `cudf.from_pandas` replaces the removed `cudf.DataFrame.from_pandas` at the five cuDF-only product sites (`ai_utils`, `umap_utils`, `feature_utils`) and in the test fixtures (#2043)**; the three cuDF chain differential cases that disagree with the full path on cuDF 26.2 are marked expected-failure on that line with the tracking issue, so the GPU lane reports them instead of crashing before them.
2931
- **GFQL pandas/cuDF: several single-alias `IN` (and other pushed-down) predicates across a hop no longer raise `Unalignable boolean Series` (#2020)**: the predicate pushdown filtered an alias frame by label after an earlier pushdown had already narrowed it, while the mask it evaluated carried a fresh positional index. Rows are now kept by position, which is the contract of a mask computed on the same rows; results equal the polars engine and the scalar `=` form.
3032
- **GFQL polars: a native chain whose edge alias shares its name with the column that step filters on is served instead of raising `incompatible-column-type` (#2039)**: the backward pass and the pruned re-execution now run each step on the graph's original edge columns, and a node step whose alias names its own filtered column filters the graph's node values rather than the marker of an earlier pass, so a stamped alias marker is never re-filtered as that column; pandas and polars return the same rows. A cross-engine collision matrix pins the single-hop shapes and records the multi-hop (#2049) and binding-column (#2050) forms as expected failures. The Cypher rows-route projection of such an alias on polars still declines with a typed error (pinned) and stays tracked.

graphistry/compute/gfql_fast_paths.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,9 @@ def _low_cardinality_pure_count_plan(
19931993
return None
19941994
if edge_rows > _LOWCARD_COUNT_MAX_INPUT_ROWS:
19951995
return None
1996+
from graphistry.compute.gfql.lazy import ExecutionTarget, active_target
1997+
if active_target() == ExecutionTarget.GPU:
1998+
return None # cudf-polars has no unnest map function; the group_by formulation is GPU-executable
19961999

19972000
# ``name=`` (polars >= 1.0, and the declared floor is 1.29) keeps the count column out
19982001
# of a rename, so a group key literally named ``count`` is served rather than crashing.

graphistry/tests/compute/gfql/cypher/test_grouped_aggregate_fused_polars.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,3 +1027,24 @@ def test_grouped_aggregate_projection_differential_all_shapes_all_engines(
10271027

10281028
result = _records(_graph(engine, nodes, edges).gfql(query, engine=engine))
10291029
assert result == oracle, f"{label}: projection changed the answer on {engine}"
1030+
1031+
1032+
def test_low_cardinality_count_plan_declines_on_the_gpu_target() -> None:
1033+
"""The value_counts formulation lowers to an ``unnest`` node cudf-polars cannot execute,
1034+
so on the GPU target the fused lane keeps the group_by formulation; on CPU the
1035+
value_counts plan is still taken for the same input."""
1036+
pl = _require_polars()
1037+
from graphistry.compute.gfql.lazy import ExecutionTarget, target_mode
1038+
work = pl.LazyFrame({"id": [1, 2, 3], "city": ["LA", "NY", "LA"]})
1039+
kwargs = dict(
1040+
node_col="id", group_keys=["city"], agg_specs=[("n", "count", None)],
1041+
needed_by_alias={"c": [("city", "city")]},
1042+
frames_by_alias={"c": pl.DataFrame({"id": [1, 2, 3], "city": ["LA", "NY", "LA"]})},
1043+
edge_rows=3,
1044+
)
1045+
with target_mode(ExecutionTarget.CPU):
1046+
cpu_plan = gfql_fast_paths_module._low_cardinality_pure_count_plan(work, **kwargs)
1047+
with target_mode(ExecutionTarget.GPU):
1048+
gpu_plan = gfql_fast_paths_module._low_cardinality_pure_count_plan(work, **kwargs)
1049+
assert cpu_plan is not None and "UNNEST" in cpu_plan.explain()
1050+
assert gpu_plan is None

0 commit comments

Comments
 (0)