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.
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
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-polarson 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:
MATCH (a {kind:'a'})-[*1..1]-(b)MATCH (a)-[*1..1]-(b)(unseeded)MATCH (a {kind:'a'})-[]-(b)(no window)MATCH (a {kind:'a'})-[*1..1]->(b)(directed)MATCH (a {kind:'a'})-[*1..2]-(b)MATCH (a {kind:'a'})-[*1..3]-(b)MATCH (a {kind:'a'})-[*2..3]->(b)MATCH (a)-[*3..3]->(b)MATCH (a)-[]->(b)-[*1..2]-(c)MATCH (a)-[*1..2]-(b)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_oracleThe 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.ymlis 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.