Skip to content

perf(gfql): precomputed in/out degree facts (C2 groundwork) - #1870

Merged
lmeyerov merged 2 commits into
masterfrom
perf/gfql-degree-facts
Aug 12, 2026
Merged

lmeyerov merged 2 commits into
masterfrom
perf/gfql-degree-facts

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Groundwork for the board's last remaining loss. No behaviour change yet — this lands the fact, the builder and the pins; the kernel consult follows.

Why this and not more tuning

lever q8@100k
today 13.50 ms LOSE 1.34x
+ per-type col-stats 11.37 VALID lane; 1.13x, just outside the tie band
+ kernel reformulation ~11.1 ~2.5% — measured, not enough
+ precomputed degree ~6.8 WIN, with room

The kernel core is an O(E) bincount + gather = 6.76 ms of 13.50. Precomputed degrees make the identical answer dot(indeg, outdeg) — O(N) = 0.046 ms, 148× less query work. That is a complexity-class change. The profile agrees independently: 48% self-time in the kernel, and a 2.4M-element numpy bincount is memory-bandwidth bound, so no reformulation closes the gap.

Board audit says this lands two cells: q8 (the verdict) and q9 ("as q8", 41.4 ms — the slowest query on the board, ~3.6× more absolute time than q8).

Typed, not global

q8 counts over one relationship type, so a global degree array is the wrong denominator. Keys on the existing (src, dst, type_column, type_value) registry shape.

Staleness is a wrong answer here — that is new

A stale min/max fact costs a scan; a stale degree fact returns a confident miscount. So identity+fingerprint is the correctness guard for this type, not an optimization guard, and it refuses on any mismatch. Likewise an endpoint outside the proved interval declines rather than clamping — a clamp would silently miscount.

Pins: dot-vs-gather-sum equivalence on hand and random graphs, every decline, rebind/engine invalidation, typed-vs-global key separation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi

lmeyerov and others added 2 commits August 11, 2026 18:03
The last board L is q8@100k. Measured levers: per-type col-stats reach 11.37ms
(1.13x, just outside the tie band, VALID lane); reformulating the kernel buys
~2.5% and is not enough. The kernel core is an O(E) bincount+gather = 6.76ms of
13.50. Precomputed degree makes it dot(indeg, outdeg), O(N) = 0.046ms -- 148x
less query work. Complexity class, not constant factor.

TYPED, not global: q8 counts over one relationship type, so a global degree array
is the wrong denominator. Keys on the existing (src, dst, type_column,
type_value) shape.

Staleness is a WRONG ANSWER for this fact kind, which is new -- a stale min/max
costs a scan, a stale degree returns a confident miscount. So the
identity+fingerprint contract is the correctness guard here, and an endpoint
outside the proved interval DECLINES rather than clamping.

This lands the fact + builder + pins; the kernel consult follows, so nothing
changes behaviour yet.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
CI mypy: ArrayLike is a deliberately small numpy/cupy surface and did not declare
min/max, which the degree builder uses for its interval precondition.

Added them to the PROTOCOL rather than casting or ignoring at the call site --
they are universal on both backends, so the protocol was simply incomplete.
Typed SupportsInt, not Any: every caller does int(arr.min()), so that is the
actual contract and it keeps the explicit-any ratchet clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
@lmeyerov
lmeyerov merged commit 9bf09b3 into master Aug 12, 2026
69 checks passed
pull Bot pushed a commit to admariner/pygraphistry that referenced this pull request Aug 12, 2026
…n O(N) dot

Wires the facts graphistry#1870 landed. sum_e indeg(src(e)) == dot(indeg, outdeg), so with
per-type degrees resident the kernel answers without touching the edges: 6.76ms
of query work becomes 0.046ms at board scale. Targets q8@100k (the board's last
LOSS) and q9 (same shape, slowest cell).

Two design points found while wiring, both pinned because a VALUE test cannot
see either:

- The node-label <-> relationship-type pairing is QUERY-dependent, not resolvable
  at index time (labels and rel types are different vocabularies). So degrees are
  built over the whole dense node interval and the consult SLICES to the proved
  domain -- sound because the bounds proof already guarantees every filtered
  endpoint lies inside it. An off-by-one there silently MISCOUNTS.
- Identity must anchor to the BOUND edge frame, not the per-type subset the fact
  was counted over. Anchoring to the subset made every lookup miss: facts built,
  never used, answers still correct. Only an engagement probe revealed it.

Gapped node spaces build nothing -- arrays are indexed by id - lo.

13 pins incl. slice-exactness across four domain shapes and a differential vs the
scan on random typed graphs. Whole-tree mypy clean apart from two pre-existing
degrees.py errors present on a clean tree.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
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