Skip to content

perf(gfql): dense-domain proof kernel for the equal-domain two-hop count (q8) - #1844

Open
lmeyerov wants to merge 5 commits into
masterfrom
perf/gfql-q8-class-fix
Open

perf(gfql): dense-domain proof kernel for the equal-domain two-hop count (q8)#1844
lmeyerov wants to merge 5 commits into
masterfrom
perf/gfql-q8-class-fix

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

A proof-gated dense-domain kernel for the equal-domain branch of the two-hop count(*) fast path (graph-benchmark q8 class: MATCH (a {t})-[{r}]->(b {t})-[{r}]->(d {t}) RETURN count(*)). That branch spent its time restricting the rel-filtered edges to domain × domain — two semi-joins on polars, two isin masks on pandas/cuDF — not counting: profiled one-shot on the 20k graph-benchmark (249k FOLLOWS edges), 5.3 ms of the 9.3 ms polars call was that restriction.

Why it is safe

When the domain's ids form a dense integer interval [lo, hi] (n_unique == hi - lo + 1) and both endpoint columns are integer, null-free and bounded by [lo, hi], interval membership IS set membership: the restriction is provably the identity, and the degree product collapses to two O(E) bincounts plus one O(N) aligned product-sum. The proof is a handful of O(E) min/max reductions — a data property, not a query-shaped hack (dense type-partitioned ids are the idiomatic multi-table graph encoding). Any failed guard declines to the existing memoized semi-join path unchanged: out-of-domain endpoints, gapped/non-integer/null ids, empty edges, and a memory guard for domains far wider than the edge count. Engine-polymorphic via the index module's array helpers (numpy host for pandas/polars/polars-gpu, cupy for cudf). When the kernel serves, no cross-call memo is written, so one-shot and warm calls converge — removing the #1825 binding sensitivity for this shape.

Tests: T6 lane pin (kernel serves + memo path provably not entered), value parity vs the forced-decline semi-join path, offset and negative interval shifts, every decline guard pinned, and the memo-lane reachability pin under the new seam.

Measured cells (attributed: q8, both scales)

Measurements come from the COMBINED candidate build 938f22851e68918799da2bee49c88db2e2c103ee (master f875724ce + this branch + perf/gfql-singlehop-subsumed-semijoin + perf/gfql-q7-two-star-minimal-join + perf/gfql-q8-bincount-r2), with per-fix attribution by disjoint cell sets: this PR owns q8 (jointly with the stacked round-2 branch at 100k), the single-hop PR owns q1/q3/q4, the two-star PR owns q5/q6/q7.

Comparator quotes, baseline boards (results/graphbench-board-{20k,100k-v2}-20260802) vs candidate lanes (results/graphbench-board-{20k,100k}-cand-20260803, graphistry/pyg-bench#170):

Cell Baseline (master pin) Candidate (this build)
q8 @ 20k polars 8.24 ms — LOSE 2.94x polars 2.22 ms — WIN 1.27x (WEAK: slot ranges overlap)
q8 @ 100k polars 33.25 ms — LOSE 2.44x polars 14.72 ms — LOSE 1.52x (narrowed; includes the stacked shift-elision round 2)

Untouched cells did not move: cells outside this PR's attribution set changed only where attributed to the sibling branches above; the two unattributed cells (q2, q9) kept their WIN verdicts at both scales (q9 within noise; q2's improvement rides the same fused single-hop lane as the semi-join subsumption branch).

Receipts

  • Lanes: graphistry/pyg-bench#170 — results/graphbench-board-20k-cand-20260803/ and results/graphbench-board-100k-cand-20260803/ (compare.txt, SOURCE_COMMITS, lock-receipt, 1 Hz load receipts, SHA256SUMS).
  • Build SHA in both lanes' SOURCE_COMMITS: pygraphistry 938f22851e68918799da2bee49c88db2e2c103ee, pyg-bench fae1e975.
  • VALIDITY: 20k "VALID: no external process in any over-limit capture (0 receipted benchmark-own spike seconds)"; 100k "VALID: … (106 receipted benchmark-own spike seconds)".
  • Protocol: position-balanced K G I I G K K G I I G K slots, RUNS=51/WARMUP=5, per-slot medians, tie band 10%, weak-flag on overlapping slot ranges; verdicts published only where GFQL row sets match Kuzu.

Do not merge without owner review.

🤖 Generated with Claude Code

https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr

lmeyerov and others added 2 commits August 2, 2026 13:20
The equal-domain branch of the two-hop count fast path (graph-benchmark q8
class: MATCH (a {t})-[{r}]->(b {t})-[{r}]->(d {t}) RETURN count(*)) spends
its time restricting the rel-filtered edges to domain x domain -- two semi-
joins on polars, two isin masks on pandas/cuDF -- not counting. Profiled
one-shot on the 20k graph-benchmark (249k FOLLOWS edges, local CPU,
diagnosis-only): 5.3ms of the 9.3ms polars call is that restriction; the
two degree group_bys alone cost 2.1ms.

When the domain's ids form a dense integer interval [lo, hi]
(n_unique == hi - lo + 1) and both endpoint columns are integer, null-free
and bounded by [lo, hi], interval membership IS set membership: the
restriction is provably the identity and the degree product collapses to
two O(E) bincounts plus one O(N) aligned product-sum. Dense
type-partitioned ids are the idiomatic multi-table graph encoding
(offset-based global ids), so the proof -- a handful of O(E) min/max
reductions -- is a data property, not a query-shaped hack; any failed
guard declines to the existing memoized semi-join path unchanged
(out-of-domain endpoints, gapped/non-integer/null ids, empty edges, and a
memory guard for domains far wider than the edge count).

Engine-polymorphic via the index module's array helpers (numpy host for
pandas/polars/polars-gpu, cupy for cudf), same convention as the CSR
kernels. When the kernel serves, no cross-call memo is written: one-shot
and warm calls converge, removing the #1825 binding sensitivity for this
shape.

Local diagnosis-only effect (20k/100k graph-benchmark, cold binding,
values byte-identical): polars q8 9.3ms -> 1.8ms and pandas 47ms -> 27ms
at 20k; polars 52.8ms -> 31.5ms at 100k (bincount is the remaining
memory-bound floor there). The authoritative numbers must come from the
locked DGX rerun of the receipted matched q1-q9 lane.

Tests: T6 lane pin (kernel serves + memo path provably not entered), value
parity vs the forced-decline semi-join path on the same graph, offset and
negative interval shifts, every decline guard pinned, and the memo-lane
reachability pin under the new seam. The H3 memo miss/hit test's graph ids
gain a deliberate gap so it keeps pinning the memo lane it documents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
Comment thread graphistry/compute/gfql_fast_paths.py Outdated
return int(lo), int(hi) # type: ignore[arg-type]
import numpy as np
s_pd = domain_nodes[node_col]
dtype = getattr(s_pd, "dtype", None)

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.

improve typing..

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.

Restructured into a fully typed polars helper _dense_interval_polars(s: pl.Series) (TYPE_CHECKING import, zero ignores inside; isinstance narrows Series.min/max) -- the dispatcher keeps exactly one localized ignore[assignment] at the get_column engine seam (fa9d3f9).

Comment thread graphistry/compute/gfql_fast_paths.py Outdated
if not isinstance(dtype, np.dtype) or dtype.kind not in "iu":
# Extension int dtypes (pandas Int64) can hold NA; plain numpy int cannot.
return None
null_count = getattr(s_pd, "null_count", None) # cudf: null mask rides a numpy dtype

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.

improve typing...

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.

The pandas/cudf arm is now _dense_interval_indexable(s: SeriesT): s.dtype is typed directly (no getattr duck-typing); only the cudf-only null_count property keeps a documented getattr, typed as Optional[int] (fa9d3f9).

Comment thread graphistry/compute/gfql_fast_paths.py Outdated
dtype = getattr(s_pd, "dtype", None)
if not isinstance(dtype, np.dtype) or dtype.kind not in "iu":
return False
null_count = getattr(s_pd, "null_count", None)

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.

improve typing

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.

_int_col_bounds_within got the same treatment: typed _bounds_within_polars / _bounds_within_indexable helpers with one localized ignore at the dispatch seam; ignores across both proofs went 5 -> 2, zero casts added, hygiene guard unchanged (fa9d3f9).

lmeyerov and others added 2 commits August 3, 2026 12:40
Review follow-up (#1844): replace the ignore-sprayed engine arms of
_dense_int_domain_interval / _int_col_bounds_within with fully typed
per-engine helpers -- _dense_interval_polars / _bounds_within_polars
(pl.Series under TYPE_CHECKING, zero ignores inside; isinstance narrows
Series.min/max instead of ignore[arg-type]) and _dense_interval_indexable /
_bounds_within_indexable (SeriesT for pandas/cudf; s.dtype typed directly,
no getattr duck-typing on dtype). Each dispatcher keeps exactly ONE
localized ignore[assignment] at the get_column engine seam with a reason
comment; the pandas arm's __getitem__ seam needs none. Ignores in these
helpers: 5 -> 2; zero casts added (explicit-cast stays at the 133 baseline);
type-hygiene guard unchanged; mypy clean modulo pre-existing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
Review follow-up (#1844, test-sufficiency question): pin the dense-domain
kernel against an INDEPENDENT pandas merge-join oracle (not its own decline
fallback) across: (a) engine-matrix kernel-served vs forced-decline value
parity for pandas/polars, plus cudf lanes gated on a cupy.bincount NVRTC
probe (skip on cudf-ok/JIT-broken hosts); (b) id-dtype matrix -- int8/int16/
int32/int64/uint32 all serve with identical counts, pandas extension Int64
declines with the reason pinned at both proof seams, float ids decline at
both seams; (c) self-loops + duplicate multi-edges multiplicative-counting
parity; (d) table-budget boundary exact at n = 4E+1024 (serve at -1 and
exactly, decline at +1) and a large-lo/small-width interval through the
round-1 shift path (helper and full path, kernel-served proven by spy);
(e) seeded differential -- 10 fixed-seed random graphs x {dense-from-0,
dense-from-offset, gapped(decline), shuffled-rows} x both engines, verdict
recorded via kernel spy and values always equal to the oracle. 31 tests,
all deterministic; local: 29 pass + 2 cudf skips; full cypher suite fail-set
unchanged vs pre-change baseline (both empty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
lmeyerov added a commit that referenced this pull request Aug 3, 2026
Stack sync after the #1844 review follow-ups (typed per-engine dense-proof
helpers + T6B review-sufficiency suite). Union resolution: round 2's fused
_edge_cols_bounds_within is kept unchanged; the per-column
_int_col_bounds_within dispatcher over the typed arms is retained as the
seam the T6B tests pin directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
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