Skip to content

cuDF silently diverges from the pandas oracle on seeded undirected degenerate var-length (a {p:v})-[*1..1]-(b) #1798

Description

@lmeyerov

Summary

MATCH (a {prop: v})-[*1..1]-(b) — a seeded, undirected, degenerate variable-length window — returns a different count on cuDF than on the pandas oracle. No error, just a different number.

Found while adding cuDF/polars-gpu parameters to the #1787 parity tests (PR #1794). It is a separate defect from #1787: #1787 is a native-polars reconstruction gap, and the polars engines decline this exact shape, so the polars fix cannot mask it. This one is in the pandas-API traversal family, where cuDF and pandas are supposed to agree.

Repro

import cudf, pandas as pd, graphistry

nodes = pd.DataFrame({"id": [0, 1, 2, 3], "kind": ["b", "a", "a", "b"]})
edges = pd.DataFrame([(2, 2), (0, 3), (2, 2), (3, 1), (0, 3), (2, 2), (1, 1)], columns=["s", "d"])
q = "MATCH (a {kind:'a'})-[*1..1]-(b) RETURN count(*) AS c"

g_pd = graphistry.nodes(nodes, "id").edges(edges, "s", "d")
g_cu = graphistry.nodes(cudf.from_pandas(nodes), "id").edges(cudf.from_pandas(edges), "s", "d")

print(int(g_pd.gfql(q, engine="pandas")._nodes["c"].to_list()[0]))   # 9
print(int(g_cu.gfql(q, engine="cudf")._nodes["c"].to_pandas().iloc[0]))  # 1

Also reproduced on the larger SEEDED_* fixture in the parity test file: pandas 9, cuDF 5.

Environment: cuDF 25.10 / RTX 3080 Ti, and cuDF 26.02 / GB10 in graphistry/test-rapids-official:26.02-gfql-polars on dgx-spark. Not version-specific.

Scope — tightly bounded, which is the useful part

Differential fuzz, cuDF vs the pandas oracle, 40 random graphs (3–7 nodes, 3–10 edges, self-loops and parallel edges), counting diverging graphs:

shape diverging
MATCH (a {kind:'a'})-[*1..1]-(b) 23/40
MATCH (a)-[*1..1]-(b) (unseeded) 0/40
MATCH (a {kind:'a'})-[]-(b) (no window) 0/40
MATCH (a {kind:'a'})-[*1..1]->(b) (directed) 0/40
MATCH (a {kind:'a'})-[*1..2]-(b) 0/40
MATCH (a {kind:'a'})-[*1..3]-(b) 0/40
MATCH (a {kind:'a'})-[*2..3]->(b) 0/40
MATCH (a)-[*3..3]->(b) 0/40
MATCH (a)-[]->(b)-[*1..2]-(c) 0/40
MATCH (a)-[*1..2]-(b) 0/40

So all three of {seeded, undirected, max_hops == 1} are required to trigger it. Drop any one and cuDF agrees. That points at the undirected doubling interacting with the per-seed hop when the window is degenerate, i.e. compute/hop.py, not at the row pipeline.

Note the same triple is one of the shapes native polars declines under #1787 (undir-degenerate-window + a seed), for a different underlying reason. Whatever fixes the multiplicity here is worth checking against that gate.

Already pinned

graphistry/tests/compute/gfql/test_varlen_bounded_engine_parity_1787.py::test_seeded_undirected_degenerate_window_agrees_with_the_oracle

The cuDF parameter carries pytest.mark.xfail(strict=True, reason="#1798"). When this is fixed the test will fail as XPASS and must be un-xfailed — deliberately, so the fix cannot land silently.

Caveat on the signal: no CI lane runs cuDF today (ci-gpu.yml is hard-disabled), so that xfail is only evaluated on a GPU box. It is not a CI guard.

Why it went unnoticed

The GFQL test tree tests cross-engine semantics one engine at a time. This shape had pandas coverage and no cuDF coverage, so the two were never compared. That pattern is systemic — see the audit in PR #1794 and #1795.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions