Skip to content

perf(gfql/index): index-accelerated get_degrees + EXISTS-prune (#5 degree-cache / #3 membership)#1697

Open
lmeyerov wants to merge 7 commits into
feat/gfql-graphframes-benchfrom
feat/gfql-index-degrees
Open

perf(gfql/index): index-accelerated get_degrees + EXISTS-prune (#5 degree-cache / #3 membership)#1697
lmeyerov wants to merge 7 commits into
feat/gfql-graphframes-benchfrom
feat/gfql-index-degrees

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1668 (→ #1661#1658 → master). First L4 index-acceleration unit off the #1658 CSR registry.

What

Node degrees + has-edge membership fall straight out of the resident CSR adjacency index (degree = diff(group_offsets), membership = keys_sorted), turning O(E) group_by+join into an O(N) searchsorted gather.

  • index/degrees.pydegrees_from_index (per-node in/out degree, engine-native searchsorted, arbitrary node ids) + adjacency_membership_keys (Document API with pydoc #3, per-direction / undirected union).
  • get_degrees fast path — ComputeMixin.get_degrees (pandas/cudf) + get_degrees_polars (polars/polars-gpu). Feeds q8-style degree-products and any degree analytics / CALL.
  • EXISTS { (n)--() } prune-isolated_pattern_alias_keys_polars routes the bare pattern through membership instead of chain_polars.
  • Cost: none needed (bulk over all nodes always beats the scan); index_policy='off' skips; try/except → scan fallback (never wrong); strict guards decline any filtered/neq/multi-hop shape.

Perf (parity-exact throughout)

  • Prototype (1M/5M, pandas): exists_prune 342→2.7ms (128×), q8 541→0.8ms (664×).
  • Real op (200k/800k, polars): EXISTS {(n)--()} scan 1206ms → index 16.5ms (73×); scales to sub-second at the 10m receipt (was 35s).

Tests

graphistry/tests/compute/gfql/index/test_index.py: get_degrees parity ×4 engines + self-loops/isolated + policy='off'; exists_prune membership parity + policy-off. Local non-GPU green (56 index tests); cudf/polars-gpu verified on dgx.

Notes / follow-ups

🤖 Generated with Claude Code

@lmeyerov lmeyerov force-pushed the feat/gfql-graphframes-bench branch from 05bdb24 to 3d0a0c1 Compare July 6, 2026 19:51
@lmeyerov lmeyerov force-pushed the feat/gfql-index-degrees branch 2 times, most recently from 63432b9 to ae66194 Compare July 8, 2026 03:11
@lmeyerov lmeyerov force-pushed the feat/gfql-graphframes-bench branch from c4ccb86 to df71110 Compare July 8, 2026 08:55
@lmeyerov lmeyerov force-pushed the feat/gfql-index-degrees branch from ae66194 to 04bb41a Compare July 8, 2026 08:55
@lmeyerov lmeyerov force-pushed the feat/gfql-graphframes-bench branch from df71110 to 47a401d Compare July 9, 2026 01:01
@lmeyerov lmeyerov force-pushed the feat/gfql-index-degrees branch from 04bb41a to 0732157 Compare July 9, 2026 01:01
from .registry import EDGE_OUT_ADJ, EDGE_IN_ADJ, GfqlIndexRegistry


def _degree_for_nodes(adj_index: Any, node_ids: Any, xp: Any) -> Any:

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.

make typed

and not getattr(edge_op, "edge_match", None)
and getattr(edge_op, "edge_query", None) is None
and not is_lazy(base_graph._edges)
):

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.

make typed -- no getattr

lmeyerov and others added 7 commits July 8, 2026 18:47
…membership)

Read node degrees straight from a resident CSR adjacency index (degree =
diff(group_offsets), gathered per node via searchsorted) instead of the O(E)
group_by(endpoint)+join. Bulk over all nodes -> always profitable when a valid
index is resident; index_policy='off' skips; try/except falls back to the scan
path (never wrong). New graphistry/compute/gfql/index/degrees.py
(degrees_from_index), engine-native (numpy pandas/polars, cupy cudf). Wired into
ComputeMixin.get_degrees (pandas/cudf).

Prototype (1M/5M pandas): exists_prune 342->2.7ms (128x), q8 541->0.8ms (664x),
exact parity. Unit + end-to-end parity green (arbitrary node ids + self-loops).
WIP: polars get_degrees_polars wiring + index-suite parity tests + 4-engine dgx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same resident-CSR fast path (degrees_from_index) in get_degrees_polars — eager
frames only (LazyFrame would force a collect), policy 'off' skips, try/except
falls back to the group_by scan. Parity green vs group_by baseline (degree /
degree_in / degree_out, arbitrary node ids + self-loops). Covers polars +
polars-gpu (numpy-host arrays). cudf/polars-gpu GPU parity pending dgx verify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parity vs group_by scan + self-loop/isolated contract + index_policy='off'
fallback, engine-parametrized. Local: pandas/polars/polars-gpu pass; cudf needs
GPU (cupy init) — verified on dgx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…membership)

Bare EXISTS pattern (no endpoint/edge filters, no drop-self neq) = "node has an
edge in this direction" = CSR adjacency membership. Route _pattern_alias_keys_polars
through adjacency_membership_keys (union of edge_out/in_adj keys_sorted for
undirected; per-direction otherwise) instead of the O(E) chain_polars; strict guard
falls through for any richer shape. New adjacency_membership_keys in index/degrees.py.
Parity + policy-off tests. Perf (200k/800k, polars): scan 1206ms -> index 16.5ms
(73x); parity exact incl self-loop-only nodes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lmeyerov lmeyerov force-pushed the feat/gfql-index-degrees branch from 0732157 to 4cdec5a Compare July 9, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant