Skip to content

fix(gfql): index AUTO preserves resident polars frames (kills the polars-hop O(E) tax)#1767

Open
lmeyerov wants to merge 5 commits into
masterfrom
perf/gfql-index-preserve-polars-frames
Open

fix(gfql): index AUTO preserves resident polars frames (kills the polars-hop O(E) tax)#1767
lmeyerov wants to merge 5 commits into
masterfrom
perf/gfql-index-preserve-polars-frames

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Problem (the C3 "polars hop is linear" mystery — root-caused)

g.gfql_index_all() (engine=auto) on a polars graph silently replaced the polars frames with pandas copies: resolve_engine(AUTO) maps polars input frames to Engine.PANDAS (legacy input-format policy), and create_index coerces the graph into the resolved engine before building.

Consequences for every later hop(engine='polars') call:

  1. _coerce_input_formats re-converts the FULL nodes+edges frames pandas→polars per call — O(E), ~208ms of a 223ms hop at 4M edges (cProfile: pl.from_pandas new_str on the 4M-row string column + pandas _isna_string_dtype), ~1.7s at 32M.
  2. The resident pandas-engine index can never serve: get_valid requires engine match AND frame identity, and the per-call conversion breaks both — so the hop was also a scan.

This is why the C3 ladder showed pandas flat at 0.16ms while polars scaled linearly 16.6→1751ms.

Fix

_resolve_index_engine: when the caller said AUTO and the resident frames are polars, the index API resolves POLARS and indexes the frames in place (the index layer is already engine-polymorphic: numpy sidecar arrays + polars row-gather). Explicit engines are honored unchanged. Deliberately scoped to the index API — this is NOT the global engine=auto routing-policy change (#1743, on hold).

Measured (dgx GB10, container polars 1.35.2, warm median of 30, parity vs pandas indexed oracle at every rung)

edges polars seeded 1-hop before after
0.25M 16.6ms 0.169ms
1M (not measured pre-fix) 0.349ms
4M 221ms 0.800ms (276×)
16M (not measured pre-fix; 32M was 1751ms) 0.834ms

Frames verified to remain polars after gfql_index_all(); parity OK at every rung.

Tests

test_index.py::TestIndexAutoPreservesPolarsFrames (file already in POLARS_TEST_FILES):

  • AUTO keeps polars frames (identity preserved) + polars-engine index resident/valid
  • index-engagement spy: resident polars index serves hop(engine='polars'), parity vs pandas indexed oracle
  • explicit engine='pandas' still coerces (contract unchanged)

Full bin/test-polars.sh locally: failures identical to master baseline (pre-existing local polars-version drift in the cypher conformance matrix; zero new failures). Full index suite (non-GPU) 85/85.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL

…E) tax)

resolve_engine(AUTO) maps polars frames to PANDAS, so create_index
coerced-and-replaced the user's polars frames with pandas copies; every
later hop(engine='polars') re-converted the full frames per call (O(E),
~220ms at 4M edges) and the pandas-engine index could never fingerprint-
match. Resolve AUTO over resident polars frames to POLARS in the index
API only (explicit engines unchanged; NOT the global auto-policy change).

dgx (warm median 30, parity vs pandas indexed oracle): polars seeded
1-hop 221 -> 0.80ms at 4M (276x); ladder 0.17/0.35/0.80/0.83ms at
0.25M/1M/4M/16M edges vs previously linear 16.6 -> 1751ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
…rialize-first

Review findings (plans/review-pr-1767/review.md): B1 edges-only polars
graph crashed in the NODE_ID build (materialize under AUTO synthesizes
pandas; polars gathers ran on a pandas frame) — gfql_index_all now
materializes + engine-aligns BEFORE any build so all indexes land valid,
and create_index re-aligns for direct calls. M1 LazyFrame frames under
AUTO crashed — the gate now requires EAGER polars DataFrames on ALL
present frames (LazyFrame/mixed keep the legacy pandas path). Spy test
asserts the index SERVED (non-None), not merely was called. AUTO-query
trade-off documented in CHANGELOG (deferred to #1743 by policy hold).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 22, 2026
…1755)

With gfql_index_all() resident, the seeded helpers replace their three
full-frame scans (O(N) seed row, O(E) frontier isin, O(N) endpoint
gather) with node-id searchsorted + CSR adjacency gathers. Decline-gated
to the identical scan body: absent/stale index (get_valid fingerprint +
identity), non-numeric id families, seed not id-filtered. Local 50k/200k
covered shape: pandas 3.11->1.85ms, polars 1.74->1.10ms; win scales with
graph size. Pinned: parity+engagement (pandas/polars), string-id decline,
stale-index decline.

Stacked on perf/gfql-index-preserve-polars-frames (#1767) — polars index
residency requires it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 22, 2026
…1755)

With gfql_index_all() resident, the seeded helpers replace their three
full-frame scans (O(N) seed row, O(E) frontier isin, O(N) endpoint
gather) with node-id searchsorted + CSR adjacency gathers. Decline-gated
to the identical scan body: absent/stale index (get_valid fingerprint +
identity), non-numeric id families, seed not id-filtered. Local 50k/200k
covered shape: pandas 3.11->1.85ms, polars 1.74->1.10ms; win scales with
graph size. Pinned: parity+engagement (pandas/polars), string-id decline,
stale-index decline.

Stacked on perf/gfql-index-preserve-polars-frames (#1767) — polars index
residency requires it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov added a commit that referenced this pull request Jul 22, 2026
Refresh the GFQL performance and engine-selection docs to lead with
numbers measured on the 0.58.0 release tag (DGX Spark GB10, warm medians
N=30, results verified identical across engines):

- Seeded typed-hop Cypher fast path, all four engines (pandas 29.9->2.46ms
  12.1x, polars 13.8->2.28ms 6.1x, cuDF 30.1->4.89ms 6.1x, polars-gpu
  25.2->2.49ms 10.1x; native chain pandas 21.1->1.65ms 12.8x, cuDF
  23.2->3.84ms 6.0x)
- Resident-index covered-shape lookups (pandas 1.74 / polars 1.59 /
  polars-gpu 1.91 / cudf 5.78 ms) with the 0.58.0 caveat that Polars
  frames need gfql_index_all(engine='polars') explicitly (fix in PR #1767)
- Flat seeded-hop scaling on pandas: 0.159-0.164ms from 0.25M to 32M
  edges (pandas-only; polars hop not yet index-routed)
- One-keyword engine='polars' LDBC SNB SF1 seed-lookup: 1,299.6->106.1ms
  (12.3x, same build) — replaces the stale "up to ~38x" headline in
  engines/performance/quick/about/overview/index
- LDBC SNB interactive SF1 vs Neo4j 5.26, same box, warm: GFQL wins 4 of
  5 clean pairs; Neo4j wins recent-replies (reported as-is)
- OLAP multi-join vs embedded Kuzu (100k scale, engine='polars'): q8
  5.0ms vs 1,004ms (200x), q9 14.2x — with the honest inverse (Kuzu wins
  single-table aggregates 2-4x, seeded property-projection lookups
  2.4-64x)

Prior Orkut/LiveJournal bulk-sweep tables are retained but relabeled as
prior-release measurements; methodology disclosures kept and extended
with the 0.58.0 tag-sweep provenance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
pull Bot pushed a commit to admariner/pygraphistry that referenced this pull request Jul 22, 2026
New docs/source/gfql/indexing.rst: lifecycle guide to GFQL resident
indexes (gfql_index_all / gfql_index_edges / create_index / show_indexes
/ drop_index) — node-id + CSR in/out adjacency sidecars, what engages
them on 0.58.0 (seeded typed-hop fast paths incl. property RETURNs and
property-seeded lookups per graphistry#1768/graphistry#1770, direct g.hop(); general polars
chain traversal honestly noted as not yet covered), the identity +
fingerprint staleness contract with safe declines, engine notes (polars
needs gfql_index_all(engine='polars') until graphistry#1767), 0.58.0-tag measured
numbers, and a runnable end-to-end example (executed).

Wired into the GFQL toctree + recommended paths next to
index_adjacency; CHANGELOG entry under Development > Documentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
return eng


def _is_resident_index_valid(

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.

can we split out autoindexing from enabling polars by default?

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