Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Documentation
- **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>`.
- **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.

## [0.58.0 - 2026-07-22]

Expand Down
2 changes: 1 addition & 1 deletion docs/source/gfql/about.rst
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ results on every engine — see :doc:`Choosing an Engine <engines>`.

::

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

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

Expand Down
246 changes: 106 additions & 140 deletions docs/source/gfql/engines.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/source/gfql/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Recommended paths:

- New to GFQL: :doc:`overview` -> :doc:`quick` -> :doc:`where` -> :doc:`return`
- Running Cypher syntax in GFQL: :doc:`cypher` -> :doc:`quick` -> :doc:`return` -> :doc:`spec/cypher_mapping`
- Faster on CPU (no GPU): :doc:`engines` -> :doc:`performance` (one keyword, ``engine='polars'``, up to ~38x over pandas)
- Faster on CPU (no GPU): :doc:`engines` -> :doc:`performance` (one keyword, ``engine='polars'``, often an order of magnitude faster on query-heavy workloads)
- Performance path (intro -> engine choice -> GPU -> remote GPU): :doc:`about` -> :doc:`engines` -> :doc:`performance` -> :doc:`remote`
- 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)
- Translating existing Cypher to native GFQL: :doc:`spec/cypher_mapping`
Expand Down
2 changes: 1 addition & 1 deletion docs/source/gfql/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ Key advantages of GFQL Let:
Leveraging GPU Acceleration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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.
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.

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

Expand Down
218 changes: 206 additions & 12 deletions docs/source/gfql/performance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,168 @@
GFQL Performance: Vectorization and GPU Acceleration
====================================================

This page is the **canonical home for GFQL benchmark numbers** — the measured tables live
here (and, for the resident-index benchmarks, in :doc:`index_adjacency`), while the rest of
the docs make stable qualitative claims and link back here.

Engine speedups at a glance
---------------------------

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

.. doc-test: skip

.. code-block:: python

g.gfql(query) # engine='pandas' (default)
g.gfql(query, engine='polars') # up to ~38x faster on real graphs, same results
g.gfql(query, engine='polars') # often much faster on query-heavy workloads, same results

For example, in the release-verified sweep below, the LDBC SNB SF1 seed-lookup drops from
**1,299.6 ms** on eager pandas to **106.1 ms** with ``engine='polars'`` — a **12.3×**
one-keyword speedup, no GPU, results identical.

.. _gfql-0580-numbers:

Release-verified numbers (0.58.0)
---------------------------------

All numbers in this section were measured on the **0.58.0 release tag** on an NVIDIA DGX
Spark (GB10), warm medians over N=30 runs. The four-engine numbers (seeded fast paths,
resident index, scaling) were kept only after the result rows were asserted identical
across engines; the competitor pairs (vs Neo4j, vs Kuzu) were validated against expected
result rows and cross-database value/row-count checks.

Seeded typed-hop fast path
~~~~~~~~~~~~~~~~~~~~~~~~~~

A seeded typed hop — Cypher ``MATCH (m {id: ...})-[:T]->(p) RETURN p`` on a 50k-node /
200k-edge graph — is the bread-and-butter selective lookup. The release's fast path speeds
it up on every engine (before → after within the sweep):

.. list-table::
:header-rows: 1
:widths: 25 20 20 15

* - Engine
- Before
- After (fast path)
- Speedup
* - ``pandas``
- 29.9 ms
- **2.46 ms**
- 12.1×
* - ``polars``
- 13.8 ms
- **2.28 ms**
- 6.1×
* - ``cudf``
- 30.1 ms
- **4.89 ms**
- 6.1×
* - ``polars-gpu``
- 25.2 ms
- **2.49 ms**
- 10.1×

The native chain form of the same query is faster still: pandas 21.1 → **1.65 ms**
(12.8×), cuDF 23.2 → **3.84 ms** (6.0×).

With a resident index
~~~~~~~~~~~~~~~~~~~~~

Building the opt-in resident index once (``g.gfql_index_all()``) makes the covered-shape
seeded lookup faster again — pandas **1.74 ms**, polars **1.59 ms**, polars-gpu
**1.91 ms**, cudf **5.78 ms**.

.. warning::
**Polars + index: pass** ``engine='polars'`` **when building.** Polars frames currently
need ``g.gfql_index_all(engine='polars')`` explicitly — an AUTO build swaps Polars
frames to pandas. The fix is tracked in PR #1767.

Scaling: flat in graph size
~~~~~~~~~~~~~~~~~~~~~~~~~~~

With the resident adjacency index, a native seeded 1-hop ``g.hop()`` on pandas stays
**flat at 0.159–0.164 ms from 0.25M to 32M edges** (constant average degree 4): the index
turns the O(E) scan into an O(degree) gather, so seeded latency does not grow with the
graph. (Pandas-only today — the Polars hop path is not yet index-routed.)

vs Neo4j (LDBC SNB interactive SF1)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

.. list-table::
:header-rows: 1
:widths: 40 15 15 15 15
:widths: 30 20 20 15

* - Workload (117M edges)
* - Query
- GFQL
- Neo4j 5.26
- Winner
* - seed-lookup
- **106.1 ms**
- 143.7 ms
- GFQL
* - message-content
- **7.1 ms**
- 23.0 ms
- GFQL
* - message-creator
- **6.8 ms**
- 27.7 ms
- GFQL
* - one-hop-expand
- **111.9 ms**
- 180.7 ms
- GFQL
* - recent-replies
- 209.6 ms
- **104.0 ms**
- Neo4j

The message-creator flip shipped in this release via property-seeded resident-index
gathers (PR #1770). Neo4j still wins recent-replies — reported as-is.

OLAP multi-join
~~~~~~~~~~~~~~~

On the graph-benchmark OLAP multi-join queries at 100k-node scale with
``engine='polars'``: **q8 runs in 5.0 ms vs 1,004 ms for embedded Kuzu (200×)**; q9 is
**14.2×**.

When not to use GFQL
~~~~~~~~~~~~~~~~~~~~

Honesty matters more than a bigger number. In the same cross-DB sweep, **embedded Kuzu
wins single-table aggregates (2–4×) and seeded property-projection lookups (2.4–64×)**.
GFQL's strengths are **traversals, multi-join OLAP, and covered seeded shapes** — route by
workload, and keep a database as the system-of-record where one fits.

.. _gfql-bulk-sweep:

Bulk engine comparison (prior sweep)
------------------------------------

The numbers in this section are from an earlier, pre-0.58.0 bulk sweep on SNAP
**com-LiveJournal** (35M edges) and **com-Orkut** (117M edges) — retained as the
bulk-workload reference until rerun on a current tag.

Same query, same answers, four engines — warm-median latency on Orkut (3.1M nodes /
117M edges), measured on a single machine:

.. list-table::
:header-rows: 1
:widths: 34 16 16 16 16

* - Workload (Orkut, 117M edges)
- ``pandas``
- ``polars``
- ``cudf``
Expand All @@ -36,18 +174,73 @@ Warm-median latency, same query, identical result rows (**Orkut**, 117M edges, S
- **68 ms**
- 1005 ms
- 63 ms
* - 2-hop from 10K seeds
- 18161 ms
- 2695 ms
- 2774 ms
- **1518 ms**
* - Full out-degree aggregation
- 799 ms
- 205 ms
- 314 ms
- **167 ms**
* - 2-hop from 100K seeds (~85M output rows)
- 28822 ms
- 8215 ms
- **6002 ms**
- 8559 ms

Reading the table:

- **Polars-CPU beat pandas up to ~38x** on bulk traversal and ~4x on aggregation — **with
no GPU**. On the 1-hop workload it was ~38x faster than pandas (68 ms vs 2613 ms).
- **Polars-CPU also beat cuDF** on these shapes (68 ms vs 1005 ms on 1-hop). cuDF runs
GFQL *eagerly*, op by op (a kernel launch + a materialized intermediate per hop), while
Polars builds **one fused lazy plan and collects once**. The fused plan wins until the
work is large enough to amortize GPU launch costs.
- **Polars-GPU was fastest on heavy multi-hop** (2-hop from 10K seeds: 1518 ms) and on
aggregation — the same fused plan, executed on the GPU.
- **cuDF won the one extreme case** — a 2-hop from 100K seeds materializing ~85M output
rows (6.0 s) — where raw GPU throughput on a single massive join overtakes everything
and Polars-GPU comes under memory pressure.
- On LiveJournal (35M edges) the pattern held: 1-hop from 10K seeds was pandas 1129 ms →
polars **37 ms** (~30x).
- The CPU crossover is early: on LiveJournal subsampled (CPU, warm-median), 1-hop
traversal was 2.7× / 4.5× / 7.6× and ``WHERE``+``ORDER`` 3.0× / 3.0× / 18× over pandas
at 10K / 100K / 1M edges. The only case pandas edged out was a trivial sub-millisecond
operation (a bare node-equality filter), where its boolean mask beats Polars' plan
overhead — immaterial at <1 ms. Reproducer: ``benchmarks/gfql/index_crossover_bench.py``.

Methodology (prior sweep)
~~~~~~~~~~~~~~~~~~~~~~~~~

- Host: NVIDIA DGX Spark (GB10 Grace-Blackwell, unified memory — the memory-pressure
boundary above is partly a property of this box), RAPIDS container
``graphistry/test-rapids-official:26.02-gfql-polars``.
- Datasets: `SNAP <https://snap.stanford.edu/data/>`_ **com-LiveJournal** (35M edges),
**com-Orkut** (117M edges).
- Measurement: **warm median** after 2 warmups (5 timed runs on Orkut, 8 on LiveJournal);
every reported cell is **guarded** — the result rows are verified identical across
engines before any timing is kept.
- Reproduce: ``benchmarks/gfql/index_bulk_olap_bench.py`` (engine comparison),
``benchmarks/gfql/pandas_vs_polars.py``, and ``benchmarks/gfql/index_vs_kuzu_prepared.py``
(vs kuzu). Numbers on this page are rendered from saved runs; the page does not re-run
them.
- **LadybugDB comparison** (referenced qualitatively in :doc:`engines`): the Ladybug
figures are **their published results on their hardware**; the GFQL side ran on the host
above via ``benchmarks/gfql/bench_ladybug_cypher.py`` (5M/20M synthetic per their suite
shape, native frames per engine, warm medians) — a cross-machine comparison, disclosed
as such. GFQL won the scan-shaped ops by large margins (full node scan ~65×,
relationship property/rowid scans ~3.5–3.7×); Ladybug won the two ops backed by
persistent structure — point lookups (index seek vs columnar scan) and a cached
relationship ``COUNT(*)``.

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

How GFQL is fast
----------------
Expand Down Expand Up @@ -77,7 +270,8 @@ your workload grows into GPU territory. See :doc:`engines` for exactly when each
Next Steps
----------

- **Choose an engine**: :doc:`engines` — the full decision matrix, methodology, and reproducers.
- **Choose an engine**: :doc:`engines` — the full decision matrix and qualitative guidance.
- **Selective lookups**: :doc:`index_adjacency` — the resident index behind the flat-scaling numbers.
- **End-to-end benchmark**: :doc:`benchmark_filter_pagerank` — CPU/GPU vs Neo4j+GDS.
- **Explore GFQL**: :ref:`10min-gfql`. **Get started**: :ref:`10min-pygraphistry`.
- **Ecosystem**: `Apache Arrow <https://arrow.apache.org/>`_ and `NVIDIA RAPIDS <https://rapids.ai/>`_.
4 changes: 2 additions & 2 deletions docs/source/gfql/quick.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Basic Usage
:meth:`gfql <graphistry.compute.gfql>` sequences multiple matchers for more complex patterns of paths and subgraphs

- **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.
- **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>`.
- **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>`.

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

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

.. code-block:: python

Expand Down
Loading