Skip to content

Commit c7419a3

Browse files
lmeyerovclaude
andcommitted
fix(gfql): resident-index gate — no float-collapsing id promotes, cudf device path, polars-gpu tags, serve-asserted tests
Review findings (plans/review-pr-1768/review.md): B1 int64<->uint64 promotes to float64 and collapses ids >= 2^53 into false matches (repro'd: seed 2^62+1 matched node 2^62) — integral pairs whose promote is float now DECLINE to the exact-compare scan path. M2 spies count SERVES (non-None), not calls, so a gate regression that declines everything fails the suite. M1 native-chain hop indexed branch + reverse/EDGE_IN_ADJ now tested (serve-asserted, parity). M3 cudf ids via .dropna().values (device array; to_numpy raised on nulls and round-tripped host). M4 polars frames also accept polars-gpu-tagged indexes (same numpy sidecars + polars gather). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
1 parent 1b16040 commit c7419a3

2 files changed

Lines changed: 82 additions & 5 deletions

File tree

graphistry/compute/chain_fast_paths.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,17 @@ def _resident_seed_indexes(
6666
else:
6767
return None
6868
kind = EDGE_OUT_ADJ if direction == "forward" else EDGE_IN_ADJ
69-
adj = registry.get_valid(kind, edges_df, (src, dst), engine)
70-
nid = registry.get_valid(NODE_ID, nodes_df, (node,), engine)
69+
engines = [engine]
70+
if engine == Engine.POLARS:
71+
# an index built with explicit engine='polars-gpu' serves the same eager
72+
# polars frames (same numpy sidecars + polars row-gather)
73+
engines.append(Engine.POLARS_GPU)
74+
adj = nid = None
75+
for eng_try in engines:
76+
adj = registry.get_valid(kind, edges_df, (src, dst), eng_try)
77+
nid = registry.get_valid(NODE_ID, nodes_df, (node,), eng_try)
78+
if adj is not None and nid is not None:
79+
break
7180
if adj is None or nid is None:
7281
return None
7382
xp, _ = array_namespace(engine)
@@ -80,13 +89,24 @@ def _ids_to_key_array(vals: Any, keys: Any, xp: Any) -> Optional[Any]:
8089
dropna semantics). None when the cast is not value-safe (mismatched families
8190
like str-vs-int decline to the scan path rather than risk false matches)."""
8291
try:
83-
arr = xp.asarray(vals.to_numpy() if hasattr(vals, "to_numpy") else vals)
92+
if 'cudf' in str(type(vals).__module__):
93+
vals = vals.dropna()
94+
raw = vals.values # device array; to_numpy() raises on nulls + round-trips host
95+
elif hasattr(vals, "to_numpy"):
96+
raw = vals.to_numpy()
97+
else:
98+
raw = vals
99+
arr = xp.asarray(raw)
84100
if arr.dtype.kind == "f":
85101
arr = arr[~xp.isnan(arr)]
86102
if arr.dtype.kind not in "iuf" or keys.dtype.kind not in "iuf":
87103
return None # numeric id families only: object/str ids keep the scan path (null-object semantics)
88104
if arr.dtype != keys.dtype:
89105
common = xp.promote_types(arr.dtype, keys.dtype)
106+
if arr.dtype.kind in "iu" and keys.dtype.kind in "iu" and common.kind == "f":
107+
# int64<->uint64 promotes to float64, which collapses distinct ids
108+
# >= 2^53 into false matches; the scan path compares exactly -> decline.
109+
return None
90110
arr = arr.astype(common)
91111
return xp.unique(arr)
92112
except (TypeError, ValueError):

graphistry/tests/compute/gfql/test_seeded_typed_hop_fastpath.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,11 +647,17 @@ def _canon(self, res):
647647
return df[cols].sort_values(cols).reset_index(drop=True)
648648

649649
def _spied(self, g, q, engine, monkeypatch):
650+
# counts SERVES (non-None returns), not calls: a gate regression that
651+
# declines everything must fail these tests, not silently pass them
650652
import graphistry.compute.chain_fast_paths as cfp
651653
counts = {"n": 0}
652654
on = cfp._index_node_rows
653-
monkeypatch.setattr(cfp, "_index_node_rows",
654-
lambda *a, **k: (counts.__setitem__("n", counts["n"] + 1), on(*a, **k))[1])
655+
656+
def spy(*a, **k):
657+
out = on(*a, **k)
658+
counts["n"] += out is not None
659+
return out
660+
monkeypatch.setattr(cfp, "_index_node_rows", spy)
655661
res = g.gfql(q, engine=engine)
656662
monkeypatch.setattr(cfp, "_index_node_rows", on)
657663
return res, counts["n"]
@@ -701,3 +707,54 @@ def test_stale_index_declines_to_scan(self, monkeypatch):
701707
plain = graphistry.nodes(ndf, "id").edges(edf2, "src", "dst").gfql(self.Q, engine="pandas")
702708
assert counts["n"] == 0, "stale index must not serve"
703709
pd.testing.assert_frame_equal(self._canon(got), self._canon(plain))
710+
711+
712+
def test_native_chain_hop_indexed_parity_forward_and_reverse(self, monkeypatch):
713+
"""M1 pin: the native-chain hop helper's indexed branch (only reachable via
714+
chain ops, never Cypher) — forward (EDGE_OUT_ADJ) and reverse (EDGE_IN_ADJ),
715+
serve-asserted, parity vs the un-indexed graph."""
716+
from graphistry.compute.ast import n, e_forward, e_reverse
717+
import graphistry.compute.chain_fast_paths as cfp
718+
ndf, edf = self._frames()
719+
mk = lambda: graphistry.nodes(ndf, "id").edges(edf, "src", "dst") # noqa: E731
720+
for ops in (
721+
[n({"id": 33}), e_forward(edge_match={"type": "KNOWS"}), n({"type": "Person"})],
722+
[n({"id": 33}), e_reverse(edge_match={"type": "KNOWS"}), n({"type": "Person"})],
723+
):
724+
serves = {"n": 0}
725+
oe = cfp._index_edge_rows
726+
727+
def spy(*a, **k):
728+
out = oe(*a, **k)
729+
serves["n"] += out is not None
730+
return out
731+
monkeypatch.setattr(cfp, "_index_edge_rows", spy)
732+
got = mk().gfql_index_all().gfql(ops, engine="pandas")
733+
monkeypatch.setattr(cfp, "_index_edge_rows", oe)
734+
assert serves["n"] > 0, f"indexed hop branch did not serve for {ops[1].direction}"
735+
plain = mk().gfql(ops, engine="pandas")
736+
pd.testing.assert_frame_equal(self._canon(got), self._canon(plain))
737+
738+
def test_uint64_int64_id_mix_declines_not_collapses(self, monkeypatch):
739+
"""B1 pin: int64<->uint64 promotes to float64, which collapses ids >= 2**53
740+
into false matches; the gate must DECLINE (scan path compares exactly)."""
741+
import graphistry.compute.chain_fast_paths as cfp
742+
big = np.uint64(2**62)
743+
ndf = pd.DataFrame({"id": np.array([big, big + np.uint64(1), np.uint64(7)], dtype=np.uint64),
744+
"type": ["Person"] * 3})
745+
edf = pd.DataFrame({"src": np.array([int(big) + 1], dtype=np.int64),
746+
"dst": np.array([7], dtype=np.int64), "type": ["KNOWS"]})
747+
g = graphistry.nodes(ndf, "id").edges(edf, "src", "dst").gfql_index_all()
748+
q = f"MATCH (m {{id: {int(big) + 1}}})-[:KNOWS]->(p) RETURN p"
749+
serves = {"n": 0}
750+
on = cfp._index_node_rows
751+
752+
def spy(*a, **k):
753+
out = on(*a, **k)
754+
serves["n"] += out is not None
755+
return out
756+
monkeypatch.setattr(cfp, "_index_node_rows", spy)
757+
got = g.gfql(q, engine="pandas")
758+
monkeypatch.setattr(cfp, "_index_node_rows", on)
759+
plain = graphistry.nodes(ndf, "id").edges(edf, "src", "dst").gfql(q, engine="pandas")
760+
pd.testing.assert_frame_equal(self._canon(got), self._canon(plain))

0 commit comments

Comments
 (0)