Skip to content

Commit 5e18967

Browse files
authored
Merge pull request #1772 from graphistry/docs/gfql-0.58.0-perf-refresh
docs(gfql): refresh perf docs with 0.58.0 release-tag-verified numbers
2 parents 48e73fc + 7f13f98 commit 5e18967

7 files changed

Lines changed: 318 additions & 157 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1010

1111
### Documentation
1212
- **GFQL pay-as-you-go resident indexing user guide**: New :doc:`Pay-As-You-Go Resident Indexing <gfql/indexing>` page — the lifecycle guide to resident indexes (`gfql_index_all()` / `gfql_index_edges()` / `create_index()` / `show_indexes()` / `drop_index()`): what the node-id + CSR in/out adjacency sidecars are, what engages them on 0.58.0 (seeded typed-hop fast paths incl. property RETURNs and property-seeded lookups per #1768/#1770, direct `g.hop()`; the general polars chain traversal honestly noted as not yet covered), the staleness/validity contract (identity + fingerprint; rebind invalidates; declines are safe — identical results either way), engine notes (polars needs `gfql_index_all(engine='polars')` until #1767), 0.58.0-tag measured numbers, and a runnable end-to-end example. Wired into the GFQL toctree + recommended paths alongside :doc:`Seeded Traversal Indexes <gfql/index_adjacency>`.
13+
- **GFQL performance docs: 0.58.0 release-tag-verified numbers, siloed in one page**: `gfql/performance.rst` is now the canonical benchmark-numbers page (alongside `gfql/index_adjacency.rst` for the index benchmarks) — a benchmark rerun updates it alone. It carries the 0.58.0 tag sweep (DGX Spark GB10, warm medians N=30; four-engine numbers cross-engine parity-verified, competitor pairs validated against expected result rows): seeded typed-hop fast path across all four engines (e.g. pandas 29.9→2.46ms, 12.1×), native chain form, resident-index covered-shape lookups (with the `gfql_index_all(engine='polars')` caveat / PR #1767), flat seeded-hop scaling on pandas (0.159–0.164ms from 0.25M to 32M edges), the one-keyword `engine='polars'` LDBC SNB SF1 seed-lookup win (1,299.6→106.1ms, 12.3×), LDBC SNB interactive SF1 vs Neo4j 5.26 same-box pairs (GFQL 4 of 5; Neo4j wins recent-replies — reported as-is), OLAP multi-join vs embedded Kuzu (q8 200×, q9 14.2×) with the honest inverse (Kuzu wins single-table aggregates 2–4×, seeded property-projection lookups 2.4–64×), plus the prior Orkut/LiveJournal bulk sweep (moved from `engines.rst`, dated once) and its methodology. All other pages — `engines.rst`, `quick.rst`, `about.rst`, `overview.rst`, `index.rst` — now carry stable qualitative claims (e.g. "often an order of magnitude faster on query-heavy workloads") that link into `performance.rst` instead of inline figures, replacing the stale "up to ~38×" headlines and avoiding scattered per-claim version labels.
1314

1415
## [0.58.0 - 2026-07-22]
1516

docs/source/gfql/about.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ results on every engine — see :doc:`Choosing an Engine <engines>`.
382382

383383
::
384384

385-
g_result = g.gfql([ ... ], engine='polars') # up to ~38x over pandas on real graphs
385+
g_result = g.gfql([ ... ], engine='polars') # often much faster on query-heavy workloads
386386

387387
**Example: Force GFQL to use a GPU engine**
388388

docs/source/gfql/engines.rst

Lines changed: 106 additions & 140 deletions
Large diffs are not rendered by default.

docs/source/gfql/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Recommended paths:
3636

3737
- New to GFQL: :doc:`overview` -> :doc:`quick` -> :doc:`where` -> :doc:`return`
3838
- Running Cypher syntax in GFQL: :doc:`cypher` -> :doc:`quick` -> :doc:`return` -> :doc:`spec/cypher_mapping`
39-
- Faster on CPU (no GPU): :doc:`engines` -> :doc:`performance` (one keyword, ``engine='polars'``, up to ~38x over pandas)
39+
- Faster on CPU (no GPU): :doc:`engines` -> :doc:`performance` (one keyword, ``engine='polars'``, often an order of magnitude faster on query-heavy workloads)
4040
- Performance path (intro -> engine choice -> GPU -> remote GPU): :doc:`about` -> :doc:`engines` -> :doc:`performance` -> :doc:`remote`
4141
- Fast seeded lookups (start from known nodes, like a DB index): :doc:`indexing` (build once with ``gfql_index_all()``, reused automatically) -> :doc:`index_adjacency` (O(degree), flat in graph size, 9-28x vs Kuzu/Neo4j)
4242
- Translating existing Cypher to native GFQL: :doc:`spec/cypher_mapping`

docs/source/gfql/overview.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ Key advantages of GFQL Let:
316316
Leveraging GPU Acceleration
317317
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
318318

319-
GFQL runs the same query on four interchangeable engines, all returning identical results: ``pandas`` (CPU, default), ``polars`` (CPU columnar — up to ~38x over pandas, **no GPU**), ``cudf`` (NVIDIA GPU), and ``polars-gpu`` (NVIDIA GPU). ``engine='auto'`` resolves to ``cudf`` for cuDF input and ``pandas`` otherwise; ``polars`` / ``polars-gpu`` are explicit opt-in (``auto`` never selects them — **so a Polars-frame graph run with the default is coerced to pandas; pass** ``engine='polars'`` **to stay native**). Neither silently bridges: ``polars-gpu`` is GPU-or-error, and unsupported Polars/Cypher shapes are declined during validation, compilation, or planning before execution rather than falling back to pandas. See :doc:`Choosing an Engine <engines>` for the decision matrix and benchmarks.
319+
GFQL runs the same query on four interchangeable engines, all returning identical results: ``pandas`` (CPU, default), ``polars`` (CPU columnar — often an order of magnitude faster on query-heavy workloads, **no GPU**), ``cudf`` (NVIDIA GPU), and ``polars-gpu`` (NVIDIA GPU). ``engine='auto'`` resolves to ``cudf`` for cuDF input and ``pandas`` otherwise; ``polars`` / ``polars-gpu`` are explicit opt-in (``auto`` never selects them — **so a Polars-frame graph run with the default is coerced to pandas; pass** ``engine='polars'`` **to stay native**). Neither silently bridges: ``polars-gpu`` is GPU-or-error, and unsupported Polars/Cypher shapes are declined during validation, compilation, or planning before execution rather than falling back to pandas. See :doc:`Choosing an Engine <engines>` for the decision matrix and benchmarks.
320320

321321
When you use cuDF (GPU) dataframes with ``engine='auto'``, GFQL executes queries on the GPU for massive speedups.
322322

docs/source/gfql/performance.rst

Lines changed: 206 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,168 @@
33
GFQL Performance: Vectorization and GPU Acceleration
44
====================================================
55

6+
This page is the **canonical home for GFQL benchmark numbers** — the measured tables live
7+
here (and, for the resident-index benchmarks, in :doc:`index_adjacency`), while the rest of
8+
the docs make stable qualitative claims and link back here.
9+
610
Engine speedups at a glance
711
---------------------------
812

913
GFQL runs the **same query** on four interchangeable engines — ``pandas`` (default),
1014
``polars`` (CPU, columnar), ``cudf`` (NVIDIA GPU), and ``polars-gpu`` (GPU) — and returns
11-
**identical results** on each (differential parity is a release gate). Unsupported
12-
engine/query combinations are declined before execution during validation, compilation,
13-
or planning rather than silently falling back. The biggest, easiest win is one keyword,
14-
**no GPU required**:
15+
**identical results** on each (differential parity is a release gate; every four-engine
16+
number on this page was kept only after the result rows were verified identical across
17+
engines, and the cross-database pairs were validated against expected result rows).
18+
Unsupported engine/query combinations are declined before execution during validation,
19+
compilation, or planning rather than silently falling back. The biggest, easiest win is one
20+
keyword, **no GPU required**:
1521

1622
.. doc-test: skip
1723
1824
.. code-block:: python
1925
2026
g.gfql(query) # engine='pandas' (default)
21-
g.gfql(query, engine='polars') # up to ~38x faster on real graphs, same results
27+
g.gfql(query, engine='polars') # often much faster on query-heavy workloads, same results
28+
29+
For example, in the release-verified sweep below, the LDBC SNB SF1 seed-lookup drops from
30+
**1,299.6 ms** on eager pandas to **106.1 ms** with ``engine='polars'`` — a **12.3×**
31+
one-keyword speedup, no GPU, results identical.
32+
33+
.. _gfql-0580-numbers:
34+
35+
Release-verified numbers (0.58.0)
36+
---------------------------------
37+
38+
All numbers in this section were measured on the **0.58.0 release tag** on an NVIDIA DGX
39+
Spark (GB10), warm medians over N=30 runs. The four-engine numbers (seeded fast paths,
40+
resident index, scaling) were kept only after the result rows were asserted identical
41+
across engines; the competitor pairs (vs Neo4j, vs Kuzu) were validated against expected
42+
result rows and cross-database value/row-count checks.
43+
44+
Seeded typed-hop fast path
45+
~~~~~~~~~~~~~~~~~~~~~~~~~~
46+
47+
A seeded typed hop — Cypher ``MATCH (m {id: ...})-[:T]->(p) RETURN p`` on a 50k-node /
48+
200k-edge graph — is the bread-and-butter selective lookup. The release's fast path speeds
49+
it up on every engine (before → after within the sweep):
50+
51+
.. list-table::
52+
:header-rows: 1
53+
:widths: 25 20 20 15
54+
55+
* - Engine
56+
- Before
57+
- After (fast path)
58+
- Speedup
59+
* - ``pandas``
60+
- 29.9 ms
61+
- **2.46 ms**
62+
- 12.1×
63+
* - ``polars``
64+
- 13.8 ms
65+
- **2.28 ms**
66+
- 6.1×
67+
* - ``cudf``
68+
- 30.1 ms
69+
- **4.89 ms**
70+
- 6.1×
71+
* - ``polars-gpu``
72+
- 25.2 ms
73+
- **2.49 ms**
74+
- 10.1×
75+
76+
The native chain form of the same query is faster still: pandas 21.1 → **1.65 ms**
77+
(12.8×), cuDF 23.2 → **3.84 ms** (6.0×).
78+
79+
With a resident index
80+
~~~~~~~~~~~~~~~~~~~~~
81+
82+
Building the opt-in resident index once (``g.gfql_index_all()``) makes the covered-shape
83+
seeded lookup faster again — pandas **1.74 ms**, polars **1.59 ms**, polars-gpu
84+
**1.91 ms**, cudf **5.78 ms**.
85+
86+
.. warning::
87+
**Polars + index: pass** ``engine='polars'`` **when building.** Polars frames currently
88+
need ``g.gfql_index_all(engine='polars')`` explicitly — an AUTO build swaps Polars
89+
frames to pandas. The fix is tracked in PR #1767.
90+
91+
Scaling: flat in graph size
92+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
93+
94+
With the resident adjacency index, a native seeded 1-hop ``g.hop()`` on pandas stays
95+
**flat at 0.159–0.164 ms from 0.25M to 32M edges** (constant average degree 4): the index
96+
turns the O(E) scan into an O(degree) gather, so seeded latency does not grow with the
97+
graph. (Pandas-only today — the Polars hop path is not yet index-routed.)
98+
99+
vs Neo4j (LDBC SNB interactive SF1)
100+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22101

23-
Warm-median latency, same query, identical result rows (**Orkut**, 117M edges, SNAP):
102+
Same box, warm, against Neo4j 5.26 — GFQL wins **4 of 5** clean pairs:
24103

25104
.. list-table::
26105
:header-rows: 1
27-
:widths: 40 15 15 15 15
106+
:widths: 30 20 20 15
28107

29-
* - Workload (117M edges)
108+
* - Query
109+
- GFQL
110+
- Neo4j 5.26
111+
- Winner
112+
* - seed-lookup
113+
- **106.1 ms**
114+
- 143.7 ms
115+
- GFQL
116+
* - message-content
117+
- **7.1 ms**
118+
- 23.0 ms
119+
- GFQL
120+
* - message-creator
121+
- **6.8 ms**
122+
- 27.7 ms
123+
- GFQL
124+
* - one-hop-expand
125+
- **111.9 ms**
126+
- 180.7 ms
127+
- GFQL
128+
* - recent-replies
129+
- 209.6 ms
130+
- **104.0 ms**
131+
- Neo4j
132+
133+
The message-creator flip shipped in this release via property-seeded resident-index
134+
gathers (PR #1770). Neo4j still wins recent-replies — reported as-is.
135+
136+
OLAP multi-join
137+
~~~~~~~~~~~~~~~
138+
139+
On the graph-benchmark OLAP multi-join queries at 100k-node scale with
140+
``engine='polars'``: **q8 runs in 5.0 ms vs 1,004 ms for embedded Kuzu (200×)**; q9 is
141+
**14.2×**.
142+
143+
When not to use GFQL
144+
~~~~~~~~~~~~~~~~~~~~
145+
146+
Honesty matters more than a bigger number. In the same cross-DB sweep, **embedded Kuzu
147+
wins single-table aggregates (2–4×) and seeded property-projection lookups (2.4–64×)**.
148+
GFQL's strengths are **traversals, multi-join OLAP, and covered seeded shapes** — route by
149+
workload, and keep a database as the system-of-record where one fits.
150+
151+
.. _gfql-bulk-sweep:
152+
153+
Bulk engine comparison (prior sweep)
154+
------------------------------------
155+
156+
The numbers in this section are from an earlier, pre-0.58.0 bulk sweep on SNAP
157+
**com-LiveJournal** (35M edges) and **com-Orkut** (117M edges) — retained as the
158+
bulk-workload reference until rerun on a current tag.
159+
160+
Same query, same answers, four engines — warm-median latency on Orkut (3.1M nodes /
161+
117M edges), measured on a single machine:
162+
163+
.. list-table::
164+
:header-rows: 1
165+
:widths: 34 16 16 16 16
166+
167+
* - Workload (Orkut, 117M edges)
30168
- ``pandas``
31169
- ``polars``
32170
- ``cudf``
@@ -36,18 +174,73 @@ Warm-median latency, same query, identical result rows (**Orkut**, 117M edges, S
36174
- **68 ms**
37175
- 1005 ms
38176
- 63 ms
177+
* - 2-hop from 10K seeds
178+
- 18161 ms
179+
- 2695 ms
180+
- 2774 ms
181+
- **1518 ms**
39182
* - Full out-degree aggregation
40183
- 799 ms
41184
- 205 ms
42185
- 314 ms
43186
- **167 ms**
187+
* - 2-hop from 100K seeds (~85M output rows)
188+
- 28822 ms
189+
- 8215 ms
190+
- **6002 ms**
191+
- 8559 ms
192+
193+
Reading the table:
194+
195+
- **Polars-CPU beat pandas up to ~38x** on bulk traversal and ~4x on aggregation — **with
196+
no GPU**. On the 1-hop workload it was ~38x faster than pandas (68 ms vs 2613 ms).
197+
- **Polars-CPU also beat cuDF** on these shapes (68 ms vs 1005 ms on 1-hop). cuDF runs
198+
GFQL *eagerly*, op by op (a kernel launch + a materialized intermediate per hop), while
199+
Polars builds **one fused lazy plan and collects once**. The fused plan wins until the
200+
work is large enough to amortize GPU launch costs.
201+
- **Polars-GPU was fastest on heavy multi-hop** (2-hop from 10K seeds: 1518 ms) and on
202+
aggregation — the same fused plan, executed on the GPU.
203+
- **cuDF won the one extreme case** — a 2-hop from 100K seeds materializing ~85M output
204+
rows (6.0 s) — where raw GPU throughput on a single massive join overtakes everything
205+
and Polars-GPU comes under memory pressure.
206+
- On LiveJournal (35M edges) the pattern held: 1-hop from 10K seeds was pandas 1129 ms →
207+
polars **37 ms** (~30x).
208+
- The CPU crossover is early: on LiveJournal subsampled (CPU, warm-median), 1-hop
209+
traversal was 2.7× / 4.5× / 7.6× and ``WHERE``+``ORDER`` 3.0× / 3.0× / 18× over pandas
210+
at 10K / 100K / 1M edges. The only case pandas edged out was a trivial sub-millisecond
211+
operation (a bare node-equality filter), where its boolean mask beats Polars' plan
212+
overhead — immaterial at <1 ms. Reproducer: ``benchmarks/gfql/index_crossover_bench.py``.
213+
214+
Methodology (prior sweep)
215+
~~~~~~~~~~~~~~~~~~~~~~~~~
216+
217+
- Host: NVIDIA DGX Spark (GB10 Grace-Blackwell, unified memory — the memory-pressure
218+
boundary above is partly a property of this box), RAPIDS container
219+
``graphistry/test-rapids-official:26.02-gfql-polars``.
220+
- Datasets: `SNAP <https://snap.stanford.edu/data/>`_ **com-LiveJournal** (35M edges),
221+
**com-Orkut** (117M edges).
222+
- Measurement: **warm median** after 2 warmups (5 timed runs on Orkut, 8 on LiveJournal);
223+
every reported cell is **guarded** — the result rows are verified identical across
224+
engines before any timing is kept.
225+
- Reproduce: ``benchmarks/gfql/index_bulk_olap_bench.py`` (engine comparison),
226+
``benchmarks/gfql/pandas_vs_polars.py``, and ``benchmarks/gfql/index_vs_kuzu_prepared.py``
227+
(vs kuzu). Numbers on this page are rendered from saved runs; the page does not re-run
228+
them.
229+
- **LadybugDB comparison** (referenced qualitatively in :doc:`engines`): the Ladybug
230+
figures are **their published results on their hardware**; the GFQL side ran on the host
231+
above via ``benchmarks/gfql/bench_ladybug_cypher.py`` (5M/20M synthetic per their suite
232+
shape, native frames per engine, warm medians) — a cross-machine comparison, disclosed
233+
as such. GFQL won the scan-shaped ops by large margins (full node scan ~65×,
234+
relationship property/rowid scans ~3.5–3.7×); Ladybug won the two ops backed by
235+
persistent structure — point lookups (index seek vs columnar scan) and a cached
236+
relationship ``COUNT(*)``.
44237

45238
There is **no universal winner**: ``polars`` typically takes over from ~10K edges up
46239
(``pandas`` still wins trivial sub-millisecond operations), and the right GPU
47240
engine depends on the workload. See :doc:`engines` for the full decision matrix, the honest
48-
"when *not* to use Polars", the cuDF-vs-Polars-GPU comparison, and the methodology + reproducer
49-
scripts behind these numbers. The end-to-end CPU/GPU-vs-Neo4j benchmark is in
50-
:doc:`benchmark_filter_pagerank`.
241+
"when *not* to use Polars", and the cuDF-vs-Polars-GPU comparison. The end-to-end
242+
CPU/GPU-vs-Neo4j pipeline benchmark is in :doc:`benchmark_filter_pagerank`, and the
243+
Spark GraphFrames head-to-head is in :doc:`benchmark_graphframes`.
51244

52245
How GFQL is fast
53246
----------------
@@ -77,7 +270,8 @@ your workload grows into GPU territory. See :doc:`engines` for exactly when each
77270
Next Steps
78271
----------
79272

80-
- **Choose an engine**: :doc:`engines` — the full decision matrix, methodology, and reproducers.
273+
- **Choose an engine**: :doc:`engines` — the full decision matrix and qualitative guidance.
274+
- **Selective lookups**: :doc:`index_adjacency` — the resident index behind the flat-scaling numbers.
81275
- **End-to-end benchmark**: :doc:`benchmark_filter_pagerank` — CPU/GPU vs Neo4j+GDS.
82276
- **Explore GFQL**: :ref:`10min-gfql`. **Get started**: :ref:`10min-pygraphistry`.
83277
- **Ecosystem**: `Apache Arrow <https://arrow.apache.org/>`_ and `NVIDIA RAPIDS <https://rapids.ai/>`_.

docs/source/gfql/quick.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Basic Usage
2727
:meth:`gfql <graphistry.compute.gfql>` sequences multiple matchers for more complex patterns of paths and subgraphs
2828

2929
- **query**: Sequence of graph node/edge matchers and optional row-pipeline call steps (for example, `rows()`, `where_rows()`, `return_()`, `order_by()`, `limit()`), or an equivalent GFQL chain object.
30-
- **engine**: Optional execution engine. Engine is typically not set, defaulting to `'auto'`. Use `'polars'` for a CPU columnar speedup (up to ~38x over pandas, no GPU), `'cudf'` or `'polars-gpu'` for NVIDIA GPU acceleration, and `'pandas'` for the default CPU path. See :doc:`Choosing an Engine <engines>`.
30+
- **engine**: Optional execution engine. Engine is typically not set, defaulting to `'auto'`. Use `'polars'` for a CPU columnar speedup (often an order of magnitude faster on query-heavy workloads, no GPU — see :doc:`Performance <performance>`), `'cudf'` or `'polars-gpu'` for NVIDIA GPU acceleration, and `'pandas'` for the default CPU path. See :doc:`Choosing an Engine <engines>`.
3131

3232
Native GFQL chains are typed Python inputs. Pass the list, dict envelope, or
3333
``Chain`` object itself; strings passed to ``g.gfql(...)`` are interpreted as
@@ -408,7 +408,7 @@ engine/query combinations are rejected before execution during validation, compi
408408
planning rather than silently falling back. Pick one with ``engine=``. See
409409
:doc:`Choosing an Engine <engines>` for the full decision matrix.
410410

411-
- **CPU columnar speedup (no GPU):** ``'polars'`` — up to ~38x over pandas on real graphs.
411+
- **CPU columnar speedup (no GPU):** ``'polars'`` — often an order of magnitude faster on query-heavy workloads, identical results (measured numbers in :doc:`Performance <performance>`).
412412

413413
.. code-block:: python
414414

0 commit comments

Comments
 (0)