[EXPERIMENTAL] perf(gfql): seeded-chain fast path — slices 1-2 shipped, slice 3 (lean combine) in design#1738
Draft
lmeyerov wants to merge 16 commits into
Draft
[EXPERIMENTAL] perf(gfql): seeded-chain fast path — slices 1-2 shipped, slice 3 (lean combine) in design#1738lmeyerov wants to merge 16 commits into
lmeyerov wants to merge 16 commits into
Conversation
lmeyerov
force-pushed
the
feat/gfql-polars-rows-entity-groupby
branch
from
July 19, 2026 20:29
e3260ad to
3e9840a
Compare
lmeyerov
force-pushed
the
perf/gfql-seeded-chain-fastpath
branch
from
July 19, 2026 20:30
3a38a0c to
b469dd0
Compare
lmeyerov
force-pushed
the
feat/gfql-polars-rows-entity-groupby
branch
from
July 19, 2026 21:15
3e9840a to
509d564
Compare
lmeyerov
force-pushed
the
perf/gfql-seeded-chain-fastpath
branch
from
July 19, 2026 21:17
b469dd0 to
2a65690
Compare
lmeyerov
force-pushed
the
feat/gfql-polars-rows-entity-groupby
branch
from
July 19, 2026 21:33
509d564 to
48a03d5
Compare
lmeyerov
force-pushed
the
perf/gfql-seeded-chain-fastpath
branch
from
July 19, 2026 21:35
2a65690 to
46e3386
Compare
lmeyerov
force-pushed
the
feat/gfql-polars-rows-entity-groupby
branch
from
July 19, 2026 23:49
48a03d5 to
74fe4a9
Compare
lmeyerov
force-pushed
the
perf/gfql-seeded-chain-fastpath
branch
2 times, most recently
from
July 20, 2026 00:43
7d46860 to
551cc63
Compare
…gmentation caches) IS1 profiling found two linear-with-E costs per seeded Cypher call on a resident graph: the index path's simple-equality keep-mask rebuilt via a full column compare (eq_str), and the polars chain's per-call with_row_index O(E) copy whose fresh identity also defeated downstream id-keyed caches. Both now recycle-safe caches keyed on the resident frame (weakref.finalize eviction, the _pl_nan_to_null pattern). Profiled: both terms eliminated; 274 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
…lice 2) The _LazyShim construction eagerly with_row_index-copied both frames O(N)+O(E) per call; lazy with_row_index numbers rows identically inside the single fused collect. 283 tests pass (index + conformance + new regressions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov
force-pushed
the
perf/gfql-seeded-chain-fastpath
branch
from
July 20, 2026 03:50
551cc63 to
e4cc21b
Compare
lmeyerov
changed the base branch from
feat/gfql-polars-rows-entity-groupby
to
master
July 20, 2026 03:50
…slice 3) The Track-B combine semi-joins the FULL node/edge frames and re-sorts on in-plan row indices EVERY call — 55ms of the ~72ms SF1 seed-lookup wall (probe /tmp/probe_collects.py). When the chain added __gfql_edge_index__ itself, surviving edge ids ARE row positions: a sorted position gather rebuilds the exact edge frame in original row order for O(result), and node rows come the same way via a recycle-safe cached id->row-position mapping (4th cache of the weakref.finalize family; frames with null/dup ids cache a None verdict and stay on Track B). Per-step endpoint semi-joins + alias flags run only on the small step/gathered frames, mirroring _combine_edges/_combine_nodes semantics exactly. Guards -> Track B fallback: multihop, pre-bound edge ids, non-node first step, unseeded chains, >100k total step rows, unsuitable node frame. Parity: 1000/1000 polars chain tests incl. new lean-vs-TrackB differential suite (byte-identical frames + engagement assertions + fallback pins for dup-id and pre-bound-edge-id graphs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
…acks DataFrame.gather) The SF1 re-measure on the 26.02 RAPIDS image (polars 1.35.2) failed every lean-engaged query with AttributeError — DataFrame.gather is a 1.4x addition. __getitem__ row selection is equivalent and supported on both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The lean attempt batch-collects step frames to bound result size; on
guard failure it previously returned None and Track B re-executed the
whole traversal inside its fused collect — +34-43% on big-wavefront SF1
queries (new-topics, job-referral). Return the materialized steps
instead and thread them into Track B, so the traversal runs once either
way. Protocol: ('lean', nodes, edges) | ('fallback', steps, labels) |
None (pre-collect declines).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Full forward label frames (hub fan-out) were eagerly collected by the lean attempt while Track B only ever touches them through fused lazy semi-joins with pushdown — new-topics cypher +33% at SF1. The lean combine needs just prev/next node-id sets and named-edge eid sets, so collect those projections in the same batched collect_all; on guard fallback label steps return to Track B untouched (lazy, fusible). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Pipelines that rebind node frames per call (whole-entity aggregation) never hit the mapping cache and paid O(N log N) sort+unique per call just to land in the guard fallback (new-topics cypher +35% at SF1). Build the mapping only once the small-result guard passes; unsuitable node frames now also return the materialized-steps fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The eager step collect materialized full wavefront columns; both the lean gathers and Track B rebuild output rows from the RESIDENT frames and read only ids/endpoints/alias flags from steps — Track B's fused plan got this for free via column pushdown, the explicit collect must project. 1136 chain+conformance tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The mapping build (O(N log N) sort + O(N) dup probe) ran per chain call for pipelines that rebind node frames (fresh identity, zero cache reuse) — the whole-entity aggregation path paid it every call (new-topics cypher +32% at SF1). First sighting of a node frame now marks the cache and serves nodes via the O(N) order-preserving is_in filter (same rows and order as the semi-join); only a recurring frame builds the mapping. Dup-id results (Track B dedupes with a first-occurrence pick) divert to the materialized-steps fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
…o fused Track B Full collection of projected step frames still executed the unpruned traversal for big-wavefront queries (new-topics cypher +34% at SF1) — Track B's single fused plan pushes the combine's semi-join predicates INTO the traversal, which a split collect cannot. Collect each lazy step frame limit-capped at guard+1: a frame at <= cap is complete and reusable; any truncated frame aborts the lean attempt so Track B runs on the ORIGINAL lazy steps with full fusion, probe cost bounded by the cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
In-memory limit pushdown cannot early-stop joins, so a truncated probe means the traversal ran once for the probe and again in Track B — new-topics cypher paid 2x traversal (+36%) on every call. Compiled-plan caching reuses the ops objects across calls: memoize the truncation per (edges frame id, ops ids), recycle-safe via weakref.finalize, so warm calls go straight to the fused Track B plan. A recycled key can only skip an eligible lean attempt — conservative, never incorrect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The skip-memo keyed on frame identity never fires for pipelines that rebind frames per call — every call is a fresh id, so new-topics cypher kept paying the probe's full traversal (+35%). The augmented-edges cache already distinguishes the cases: a HIT means this edges frame recurred (resident graph), a MISS means per-call frames. Require the hit before attempting the lean probe; resident graphs warm into the lean path on their second call, rebound-frame pipelines go straight to fused Track B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The trace on the real new-topics run showed the same edges frame recurring but FRESH op objects per call — the identity-keyed memo could never hit, so the truncating probe re-ran the traversal every call (+40%). Key the memo on the structural chain shape instead (op kinds, directions, hops, aliases, filter KEYS — seed values vary while fan-out shape doesn't); collisions can only skip an eligible attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
LazyFrame.schema / .columns resolve the schema AND emit a PerformanceWarning on every access; both ran per predicate per call on the seeded fast path (the SF1 profile showed the two warning sites firing on every query). Route polars schema reads through collect_schema via a local _schema_of, and give filter_by_dict a lazy-safe column probe. Zero PerformanceWarnings now; 200k/1M IS1 warm 12.1 -> 11.4ms. Deliberately avoids row_pipeline.py so this does not conflict with the pending #1714 OLAP rebase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
_select_emits_temporal_constructor_text called the per-column _has_temporal_constructor_text helper in a loop, each doing its own select().item() collect — 6 collects per call on LDBC IS1. Scan every String column in ONE fused select instead (same ^-anchored pattern, same any() semantics, same swallow-failures-as-False parity), and read the schema lazily-safely. 71 -> 66 collects/call on the 200k/1M probe. 1233 chain+row-pipeline+conformance tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Stack PR #6 (base: #1737 → #1736 → #1734 → #1735 → master). First slice of the evidence-driven Cypher-Search fast path (plan.md 'FAST-PATH EVIDENCE'): eliminates the two per-call O(E) costs that made seeded Cypher latency GROW with edge count (LDBC SF0.1→SF1 grew 4-5× on 11× data; Kuzu stays flat).
gfql/index/traverse.py): the feat(gfql): physical adjacency indexes for O(degree) seeded traversal #1658 index path rebuilt its simple-equality edge keep-mask via a full column compare (eq_str, ~2.8ms/1M rows) on EVERY seeded hop. Now cached per (resident frame, engine, scalar edge_match), recycle-safe viaweakref.finalize(the perf(gfql): identity-stable _pl_nan_to_null + clean-cache (kills O(E) per-hop NaN re-scan on polars) #1735 clean-cache pattern).lazy/engine/polars/chain.py): the polars chain rebuiltwith_row_indexper call — an O(E) copy whose fresh object identity ALSO defeated every downstream id-keyed cache (including Replace NaNs with nulls since node cannot parse JSON with NaNs #1). One augmentation per resident frame; stable identity restored.binding_rows_polarsfor the bindings-row path, same pattern.)Evidence
eq_str2.8ms/call +with_row_indexper call; warm median 18.7-23ms.Safety of the caches
Pure functions of the resident frame; eviction on frame GC via
weakref.finalizeso a recycled id can never serve stale data while the source frame is alive; rebindingg.edges(...)yields a new frame object → new key → recompute (semantics unchanged).🤖 Generated with Claude Code
https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL