From 7b6adc0859d5ec131d2b610389eb560eb5937a2b Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 10:04:20 -0700 Subject: [PATCH 01/29] docs(gfql): start-here nav with perf/reference hubs; result-first benchmark pages - gfql/index: Start Here (about, overview, quick, cypher) + Guides hubs gfql/perf/index and gfql/reference/index + Developer Resources; no page moved, URLs unchanged; loading_graph_data joins the reference hub - benchmark_filter_pagerank, benchmark_graphframes: lede states result and engine to use; one "Method and limits" section; numbers unchanged and re-verified against bench cells / results.json; Cypher comments use // - CHANGELOG: [Development] / Docs Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 4 + .../source/gfql/benchmark_filter_pagerank.rst | 151 ++--- docs/source/gfql/benchmark_graphframes.rst | 515 ++++++------------ docs/source/gfql/index.rst | 90 +-- docs/source/gfql/perf/index.rst | 24 + docs/source/gfql/reference/index.rst | 24 + 6 files changed, 310 insertions(+), 498 deletions(-) create mode 100644 docs/source/gfql/perf/index.rst create mode 100644 docs/source/gfql/reference/index.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index a3304fa5b2..b84891dc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * GFQL: the general chain path keeps integer and boolean node attribute dtypes on a closed graph: the endpoint-closure backfill now appends only endpoints missing from the node frame instead of concatenating every endpoint id and deduplicating afterwards, which widened the attribute columns to float on every route that fell through the hot paths (#2058, chain seam; the row-pipeline pivot dtypes are a separate item). * GFQL: the chain specializations move into `graphistry/compute/chain_specializations/{admission,hotpaths}.py` (pandas/cuDF single-node lane, seeded typed single hop, seeded typed RETURN-destination) and `graphistry/compute/gfql/lazy/engine/polars/chain_specializations/{admission,hotpaths}.py` (polars plain single-hop branches, seeded lane, RETURN-destination), each lane next to the admission predicate the dispatcher calls (`native_fast_path_admits`, `polars_plain_single_hop_admits`, `polars_seeded_lane_admits`); `chain.py` and the polars chain only dispatch, `chain_fast_paths.py` keeps the shared seed/index helpers. No route admits or declines anything it did not before. Tests mirror the new paths and filter one shared shape corpus per route with the route's own gate; `GFQL_ROUTES_OFF=` (test conftest) makes named hot paths decline so every existing test replays through the other routes, and `bin/test-routes-off.sh` reports the per-route divergences. * GFQL: the wavefront seed-rediscovery rule moved out of `hop.py` into `graphistry/compute/gfql/seed_rediscovery.py` (pandas/cuDF) and `graphistry/compute/gfql/lazy/engine/polars/seed_rediscovery.py` (polars); `undirected_rediscovered_seed_ids` (an internal helper) is gone. +### Docs + +- **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. +- **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` and `gfql/benchmark_graphframes` open with the measured outcome and the engine to use, keep every number unchanged, and move measurement rules into one "Method and limits" section per page. GraphFrames literals were re-verified against `gfql/_static/graphframes/results.json`; the filter/PageRank page still prints from the vendored pyg-bench artifact. ## [0.59.0 - 2026-08-31] diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index 9e05965acd..2413543b9e 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -6,9 +6,14 @@ GFQL Cypher Filter + PageRank Benchmark :width: 160px :align: right -Run Cypher queries and graph analytics directly on Python dataframes, without a -database. This benchmark compares **Graphistry's local Cypher** on CPU and GPU -with **Neo4j + GDS** for the same three-stage pipeline. +One ``g.gfql(...)`` call runs a three-stage pipeline (filter, PageRank, filter) +on a dataframe already loaded in Python. On the 30M-edge GPlus graph, the GFQL +GPU path finishes in :bench:`pagerank.gplus.gfql_gpu` and the GFQL CPU path in +:bench:`pagerank.gplus.gfql_cpu`. The same pipeline on Neo4j + Graph Data +Science (GDS) takes :bench:`pagerank.gplus.neo4j_gds`. Use the GPU engine for +this workload when one is available. It is +:bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster than the CPU engine on +GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. .. list-table:: :header-rows: 1 @@ -30,21 +35,19 @@ with **Neo4j + GDS** for the same three-stage pipeline. - :bench:`pagerank.gplus.gfql_gpu` - :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` -Each time covers the full search → PageRank → search pipeline after warm-up. GFQL -reuses data already loaded in Python. Neo4j includes server calls and rebuilds the -in-memory graph used by Graph Data Science (GDS) for each timed iteration. The table -therefore shows direct pipeline times, not a GFQL-to-Neo4j speedup ratio. +Each time is the warm end-to-end pipeline. The Neo4j and GFQL columns are +direct pipeline times under different profiles, so the table gives no +GFQL-to-Neo4j ratio. See :ref:`pagerank-method`. -For the same GFQL query, the GPU path is -:bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter and -:bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster on the 30M-edge GPlus graph. +.. image:: _static/filter_pagerank/twitter_pipeline.svg + :alt: Twitter warm pipeline time: Neo4j + GDS 11.72s, GFQL Cypher CPU 1.58s, GFQL Cypher GPU 0.24s + +.. image:: _static/filter_pagerank/gplus_pipeline.svg + :alt: GPlus warm pipeline time: Neo4j + GDS 354.47s, GFQL Cypher CPU 32.10s, GFQL Cypher GPU 2.42s The pipeline ------------ -One ``g.gfql(...)`` call searches the graph, calculates PageRank, and searches -the result: - .. code-block:: python # pip install graphistry @@ -70,72 +73,41 @@ the result: engine="cudf", # or "pandas" with igraph backend ) -- ``GRAPH g1``: find high-degree nodes and their neighbors -- ``GRAPH g2``: enrich ``g1`` with PageRank scores (igraph on CPU, cugraph on GPU) -- Final ``GRAPH``: keep high-PageRank nodes and their neighbors +- ``GRAPH g1``: keep high-degree nodes and their neighbors. +- ``GRAPH g2``: add PageRank scores to ``g1`` (igraph on CPU, cugraph on GPU). +- Final ``GRAPH``: keep high-PageRank nodes and their neighbors. -Choose a CPU or GPU backend without changing the query: +The query does not change between engines: - **CPU**: ``engine="pandas"``, ``backend="igraph"`` - **GPU**: ``engine="cudf"``, ``backend="cugraph"`` -The Neo4j version requires Cypher, a separate in-memory graph for GDS, and several -writes. See :ref:`neo4j-analog` below. - -Twitter (2.4M edges): reported pipeline timings ------------------------------------------------- - -.. image:: _static/filter_pagerank/twitter_pipeline.svg - :alt: Twitter warm pipeline time: Neo4j + GDS 11.72s, GFQL Cypher CPU 1.58s, GFQL Cypher GPU 0.24s - -- **Neo4j + GDS**: :bench:`pagerank.twitter.neo4j_gds` - -- **GFQL Cypher on CPU** (pandas + igraph): :bench:`pagerank.twitter.gfql_cpu` - -- **GFQL Cypher on GPU** (cuDF + cuGraph): :bench:`pagerank.twitter.gfql_gpu` — - :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster than the GFQL CPU path - -GPlus (30M edges): larger graph -------------------------------- - -.. image:: _static/filter_pagerank/gplus_pipeline.svg - :alt: GPlus warm pipeline time: Neo4j + GDS 354.47s, GFQL Cypher CPU 32.10s, GFQL Cypher GPU 2.42s - -- **Neo4j + GDS**: :bench:`pagerank.gplus.neo4j_gds` -- **GFQL Cypher on CPU** (pandas + igraph): :bench:`pagerank.gplus.gfql_cpu` -- **GFQL Cypher on GPU** (cuDF + cuGraph): :bench:`pagerank.gplus.gfql_gpu` — - :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster than the CPU path - -GPlus is 12x the edges of the Twitter graph, and the GPU pipeline still answers in -seconds. - -What this shows ---------------- - -GFQL runs the same query on pandas + igraph or cuDF + cuGraph. The GPU path was -faster on both graphs. GFQL also keeps dataframe processing, graph search, and -analytics in one Python process. +Each ``GRAPH { }`` block takes a graph as input and returns a graph. Intermediate +graphs stay in Arrow, pandas, or cuDF memory in the same Python process. GFQL +returns the same result on every engine or rejects the query before execution; +see :doc:`engines` for the parity rules. .. _neo4j-analog: Neo4j + GDS analog ------------------ -The Neo4j equivalent of the same pipeline: +The Neo4j version writes marker properties at each stage and projects a +separate in-memory graph for GDS: .. code-block:: cypher - -- 1. Mark seed nodes by degree + // 1. Mark seed nodes by degree MATCH (n:Node) SET n.seed = n.degree >= $cutoff; - -- 2. Expand one hop from seeds + // 2. Expand one hop from seeds UNWIND $seed_ids AS sid MATCH (s:Node) WHERE id(s) = sid MATCH (s)-[r:LINK]-(target:Node) SET target.in_subgraph = true, r.in_subgraph = true; - -- 3. Project subgraph and run PageRank + // 3. Project subgraph and run PageRank CALL gds.graph.project.cypher( 'subgraph', 'MATCH (n:Node) WHERE n.in_subgraph RETURN id(n) AS id', @@ -147,7 +119,7 @@ The Neo4j equivalent of the same pipeline: ); CALL gds.pageRank.write('subgraph', {writeProperty: 'pagerank'}); - -- 4. Keep high-PageRank core + one hop + // 4. Keep high-PageRank core + one hop MATCH (n:Node) WHERE n.pagerank >= $cutoff SET n.core = true; UNWIND $core_ids AS cid @@ -155,48 +127,35 @@ The Neo4j equivalent of the same pipeline: MATCH (c)-[r:LINK]-(target:Node) SET target.final = true, r.final = true; -Why the GFQL pipeline is shorter --------------------------------- - -The Neo4j version is longer because its stages write flags to database records -and create a separate GDS graph. GFQL passes a graph directly from one stage to -the next. - -**Graphs as values.** Each ``GRAPH { }`` block receives a graph, changes it, and -passes a graph to the next block. This removes the property flags, separate GDS -projections, and batched writes used in the Neo4j example. - -**One query, multiple engines.** GFQL compiles Cypher to dataframe operations. -Set ``engine="pandas"`` for CPU execution or ``engine="cudf"`` for GPU execution. -See :doc:`cypher` for supported Cypher features and :doc:`overview` for the GFQL -design. - -**Columnar data in Python.** Intermediate graphs stay in Arrow, pandas, or cuDF -memory. ETL, search, and analytics can remain in the same Python pipeline. - -**Consistent results.** GFQL either returns the same result on an engine or rejects -the query before execution. It does not silently change engines. See :doc:`engines` -for the parity and validation rules. - -This page is one workload (a filter → PageRank → filter pipeline) against one -external baseline (Neo4j + GDS). For the full four-engine picture — when Polars -beats pandas on CPU, when the GPU pulls ahead, and how to choose — see -:doc:`engines`. For seeded lookups, see :doc:`index_adjacency`. - -For more on GFQL: - -- :doc:`engines` — choosing pandas / Polars / cuDF / Polars-GPU -- :doc:`index_adjacency` — seeded-traversal CSR adjacency index -- :doc:`cypher` — Cypher syntax through ``g.gfql("MATCH ...")`` -- :doc:`overview` — GFQL design, features, and GPU acceleration -- :doc:`about` — 10-minute introduction to GFQL +.. _pagerank-method: + +Method and limits +----------------- + +- **Workload**: one pipeline (filter, PageRank, filter) on two SNAP graphs. + In the GPlus locked run, the Neo4j and GFQL arms selected exactly the same + node set. +- **Timing**: warm runs after warm-up. The GPlus Neo4j time comes from a + later locked run of twelve position-balanced slots on one machine; the + provenance blocks below name each run. +- **Profiles differ**: GFQL reuses frames already resident in Python. Neo4j + includes server round trips, writes marker properties in both filter stages, + and rebuilds the GDS in-memory projection on every timed iteration. The + Neo4j column is therefore a direct pipeline time, not an engine-primitive + time, and no GFQL-vs-Neo4j ratio is published. +- **Comparable ratio**: the GPU-vs-CPU column compares the same GFQL query and + the same profile, so that ratio is published. +- **Scope**: for the four-engine CPU/GPU comparison and engine choice, see + :doc:`engines`. For seeded lookups, see :doc:`index_adjacency`. For the + Spark GraphFrames comparison, see :doc:`benchmark_graphframes`. .. _pagerank-provenance: -Benchmark environment and provenance ------------------------------------- +Provenance +---------- -Every figure is printed from ``docs/source/_data/gfql_benchmarks.json`` (pyg-bench). +Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, +which pyg-bench publishes. .. bench-provenance:: filter-pagerank-20260728 diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 5d431ff89b..f9e7fa7174 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -1,39 +1,27 @@ -GFQL Graph Benchmark: DataFrame-Native vs Apache Spark GraphFrames -================================================================== +GFQL vs Apache Spark GraphFrames on One Node +============================================ .. image:: _static/gfql-mascot.png :alt: GFQL mascot :width: 160px :align: right -.. note:: - - LiveJournal and Orkut figures are final: median of 5 timed runs after 2 - warmups, result-size parity enforced per task. One cell — LiveJournal GPU - PageRank — is median of 3 after 1 warmup (a re-run after a transient GPU - fault on the first pass); every other cell, including Orkut GPU PageRank, is - the full 5/2. Friendster (~1.8B edges) was the stretch target; our *eager - in-memory* harness runs out of RAM loading it (documented below) — this is a - harness/loader limit, not an engine ceiling. Polars' streaming engine and the - cudf-polars streaming executor are the larger-than-memory paths, not yet - benchmarked here. - -Run graph filters, k-hop neighborhoods, and PageRank directly on Python -dataframes — no cluster required. This benchmark compares **GFQL** -(Graphistry's dataframe-native graph query language) on CPU -(``engine="polars"``) and GPU (``engine="polars-gpu"``) against **Apache Spark -GraphFrames** (``local[*]``, single-node JVM) on the same tasks over large -SNAP graphs. - -The short version: for **filter and traversal**, GFQL wins decisively — even on -CPU — because a single-node columnar engine avoids the JVM startup, -task-serialization, and shuffle overhead that dominate Spark at sub-second -result sizes. For **PageRank**, the honest answer is mixed: GFQL's *CPU* path -routes through igraph and is *slower* than GraphFrames at scale; GFQL's win on -PageRank comes from the *GPU* path (cugraph). We state both plainly below. - -Headline (LiveJournal, ~35M edges) ----------------------------------- +On one machine, GFQL answers graph filters and k-hop traversals 1.3x to 43x +faster than Apache Spark GraphFrames. The CPU engine alone wins every one of +those cells. For whole-graph PageRank, use the GFQL GPU engine: it is 10x to +15x faster than GraphFrames. The GFQL CPU PageRank path (igraph) is slower +than GraphFrames. + +The workloads are four tasks on two SNAP graphs: LiveJournal (35M edges) and +Orkut (117M edges). GFQL runs with ``engine="polars"`` (CPU) and +``engine="polars-gpu"`` (GPU). GraphFrames runs on Spark ``local[*]``, a +single-node JVM using all cores. Every cell is the median of 5 timed runs +after 2 warmups, and every task returns the same result size on all three +systems. One cell, LiveJournal GPU PageRank, is the median of 3 runs after 1 +warmup. See :ref:`graphframes-method` for the full measurement rules. + +LiveJournal (35M edges) +----------------------- .. list-table:: :header-rows: 1 @@ -48,162 +36,27 @@ Headline (LiveJournal, ~35M edges) - 2.1ms - 2.4ms - 90.4ms - - **~43x** + - 43x * - **1-hop** (50 seeds) - 236.8ms - 191.4ms - 1421.7ms - - **~7.4x** + - 7.4x * - **2-hop** (50 seeds) - 1669.3ms - 1542.1ms - 3583.3ms - - **~2.3x** + - 2.3x * - **PageRank** (full graph) - 49.3s - - **1.11s** + - 1.11s - 16.3s - - **~14.7x** (GPU) / *0.33x* (CPU) - -*Median of 5 after 2 warmups (LiveJournal GPU PageRank is median of 3 — see the -note above). DGX* ``dgx-spark``, *GB10 GPU, single node; Spark* ``local[*]`` -*over all cores. Cold load (ETL) of the SNAP file is 2.4s for GFQL vs 10.3s for -GraphFrames — GFQL also loads ~4x faster.* - -Result-size parity is enforced per task: filter -returns the identical node count above threshold, 1-hop the identical -neighborhood size (**119,877**), 2-hop the identical size (**1,378,430**), and -PageRank the identical vertex count (**3,997,962**). A size mismatch flags a bug -(directedness or seed-set drift), not a speedup. - -When GFQL wins, and when it doesn't ------------------------------------ - -This page is written for a Spark GraphFrames user evaluating alternatives. -The point is not to spin — it is to be trustworthy. Two findings, both true: - -**1. Filter and traversal: GFQL wins across the board (1.3–43x; most cells 2x+), even on CPU.** -There is no JVM to warm, no task graph to serialize, no shuffle to schedule. A -single-node columnar engine is simply the right tool for sub-second graph -queries. Spark's ``local[*]`` per-query scheduler overhead dominates at these -result sizes — Spark is engineered for distributed throughput across a cluster, -not single-node latency. Note the GPU barely moves these numbers: at this scale -the CPU polars path is already fast enough that data movement, not compute, is -the floor. - -**2. PageRank: the honest result is mixed — reach for the GPU.** -GFQL's *CPU* path has no native PageRank, so the polars engine converts to -pandas and calls igraph. Single-threaded igraph is **slower than GraphFrames** -at this scale (49.3s vs 16.3s on LiveJournal, and 160s vs 37s on Orkut — the gap -widens with size): Spark's multicore iterative aggregation genuinely beats it. -GFQL's PageRank advantage comes entirely from the **GPU** path (cugraph, -~1.11s), which beats GraphFrames by ~14.7x. So the -guidance is explicit: for whole-graph analytics like PageRank, use the GPU -engine; the CPU-igraph route is a convenience, not a speed play. - -If you take one thing away: **GFQL replaces Spark for interactive single-node -graph queries, and the GPU engine additionally replaces it for whole-graph -analytics — but the CPU engine alone does not win PageRank, and we won't -pretend it does.** - -filter — WHERE on a degree column ---------------------------------- - -A ``WHERE`` on a numeric column: keep nodes with ``degree >= threshold``. SNAP -graphs carry no attributes, so ``degree`` is precomputed at cold-load (charged -to load, not to the query, for *both* systems) and used as the natural -threshold column. - -.. doc-test: skip - -.. code-block:: python - - # GFQL - from graphistry import n - from graphistry.compute.predicates.numeric import ge - g.gfql([n(filter_dict={'degree': ge(42)})], engine="polars") # or "polars-gpu" - - # GraphFrames - gf.degrees.filter("degree >= 42").count() - -LiveJournal: GFQL polars **2.1ms**, GFQL polars-gpu **2.4ms**, GraphFrames -**90.4ms** — same node count (**403,561**) on a shared degree threshold. The gap -is almost entirely Spark's per-query scheduling floor; the actual predicate is -trivial on both. - -1-hop — neighborhood from a 50-node seed set --------------------------------------------- - -Undirected 1-hop expansion from a fixed 50-node high-degree seed set. + - 14.7x (GPU); CPU is 0.33x -.. doc-test: skip - -.. code-block:: python - - # GFQL - from graphistry import n, e_undirected - g.gfql([n(filter_dict={'id': is_in(seeds)}), e_undirected(hops=1), n()], engine="polars") - -GraphFrames has no k-hop-neighborhood primitive (``bfs`` is shortest-path -between predicates, ``find`` is a fixed motif), so the Spark side expands via an -iterated undirected edge join — still pure Spark, ending in ``.count()``. - -LiveJournal: GFQL polars **236.8ms**, GFQL polars-gpu **191.4ms**, GraphFrames -**1421.7ms**, identical neighborhood size **119,877**. - -2-hop — two-hop neighborhood ----------------------------- - -Same seed set, two undirected hops (``e_undirected(hops=2)`` for GFQL; two -iterated joins for Spark). - -LiveJournal: GFQL polars **1669.3ms**, GFQL polars-gpu **1542.1ms**, GraphFrames -**3583.3ms**, identical size **1,378,430**. As the result grows, real join work -starts to dominate Spark's fixed overhead, so the multiple narrows (~2.3x) — but -GFQL still wins on a single node. - -PageRank — full-graph analytics -------------------------------- - -Full-graph PageRank (damping 0.85). GFQL CPU routes to igraph -(``g.compute_igraph('pagerank')``); GFQL GPU routes to cugraph -(``g.compute_cugraph('pagerank')``); GraphFrames uses -``gf.pageRank(resetProbability=0.15, maxIter=20)``. GraphFrames runs a fixed -20 iterations; igraph and cugraph iterate to their library-default tolerance -(igraph ``eps=1e-3``, cugraph ``tol=1e-5``). This favors neither side -uniformly — it is disclosed so the times are interpretable, not a hidden knob. - -LiveJournal (all return **3,997,962** vertices): - -.. list-table:: - :header-rows: 1 - :widths: 40 30 30 +Cold load of the SNAP file: 2.4s for GFQL, 10.3s for GraphFrames. - * - Engine / backend - - Time - - vs GraphFrames - * - GFQL polars / igraph (CPU) - - 49.3s - - *0.33x (slower)* - * - GFQL polars-gpu / cugraph (GPU) - - **1.11s** - - **~14.7x faster** - * - GraphFrames (local[*]) - - 16.3s - - 1.0x - -This is the mixed result, stated plainly. The CPU-igraph route is single -threaded and **loses to Spark's multicore aggregation** here. The GPU-cugraph -route wins by an order of magnitude. Because GraphFrames uses a fixed -``maxIter`` while igraph/cugraph iterate to a tolerance, the raw scores are not -bit-identical, so we compare **wall-clock-to-usable-scores**: the three engines -return the identical vertex set (**3,997,962**), and their PageRank rankings -agree **exactly** — pairwise Spearman rho = **1.00** and top-100 overlap -**100/100** across igraph, cugraph, and GraphFrames (parity check saved to -``bench_graphframes_pagerank_parity.json``). This is a "same ranked result, different cost" comparison, not a raced approximation. - -Orkut (~117M edges) -------------------- +Orkut (117M edges) +------------------ .. list-table:: :header-rows: 1 @@ -218,166 +71,164 @@ Orkut (~117M edges) - 1.7ms - 2.0ms - 70.6ms - - **~42x** + - 42x * - **1-hop** (50 seeds) - 562.9ms - 442.0ms - 3826.6ms - - **~8.7x** + - 8.7x * - **2-hop** (50 seeds) - 9439.8ms - 8860.2ms - 11582.9ms - - **~1.3x** + - 1.3x * - **PageRank** (full graph) - 160.1s - - **3.50s** + - 3.50s - 36.8s - - **~10.5x** (GPU) / *0.23x* (CPU) - -*Median of 5 after 2 warmups (all cells, including GPU PageRank). -Result-size parity per task: filter* **308,666**; *1-hop* **434,973**; *2-hop* -**1,991,366**; *PageRank* **3,072,441**. *Cold load 5.1s (GFQL) vs 14.7s -(GraphFrames). The pattern holds at 117M edges: GFQL wins filter/traversal -outright, the GPU wins PageRank by ~10x, and CPU-igraph PageRank falls further -behind Spark (0.23x) as the graph grows.* - -Friendster (~1.8B edges) — our eager-load harness stops here; streaming is next --------------------------------------------------------------------------------- - -Friendster (1,806,067,135 edges, 65.6M nodes) was the stretch target. Every path -we *ran* ran out of headroom on the **119 GB** node — but the honest framing is -that this is where **our benchmark harness's eager, in-memory load** stops, **not -a hard ceiling of the engines.** The harness reads the whole graph into memory up -front (``pandas.read_parquet`` → a ~29 GB edge frame, plus a second ~29 GB pass to -build the degree/node table) *before the query runs*; that materialization is what -the OS kills. + - 10.5x (GPU); CPU is 0.23x + +Cold load: 5.1s for GFQL, 14.7s for GraphFrames. + +Result sizes are identical across the three systems for every task: .. list-table:: :header-rows: 1 - :widths: 26 74 - - * - Path (as configured in this harness) - - Outcome at 1.8B edges on one 119 GB node - * - GFQL polars (CPU), eager load - - **OOM in the load**, before the query: the pandas edge frame + degree build - peak past physical RAM. The *query* engine never runs. - * - GFQL polars-gpu (GPU), eager cudf load - - **Exceeds memory in the load**: even a lean cudf-direct edge read drives the - 119 GB unified pool into swap. The in-memory GPU executor is not the - larger-than-memory path (see below). - * - GraphFrames (local[*]) - - **Swap-thrash.** A ``local[*]`` driver with a 90 GB heap on a 1.8B-edge - GraphFrame saturates memory and does not finish in usable time on one box. - -**What we did *not* run — the larger-than-memory paths that exist.** GFQL's Polars -engine already ships opt-in streaming escape hatches, and this harness did not use -them: - -- **CPU:** ``GFQL_POLARS_CPU_STREAMING=1`` collects the plan with Polars' streaming - engine (batched, spills to disk), parity-identical to the default. Paired with a - **lazy** source (``pl.scan_parquet`` instead of an eager ``pandas.read_parquet``), - the 1.8B-edge input is never fully materialized. -- **GPU:** ``GFQL_POLARS_GPU_EXECUTOR=streaming`` selects the cudf-polars *streaming* - executor — explicitly the escape hatch for **larger-than-device-memory** results, - where the default in-memory executor would OOM. - -Both are **off by default** because in-scope GFQL graphs/results fit in memory and -streaming regresses small/interactive sizes — the right default for the 35M–117M -regime this page measures. What we have *not yet* done is wire a lazy -``scan_parquet`` ingestion path through GFQL and benchmark the streaming collect at -1.8B; that is the correct larger-than-memory test (comparable to Ladybug's -out-of-core mode and to a Spark cluster) and is **tracked as follow-up work**, not a -limitation we're conceding. So: GFQL wins decisively *in-memory* through ~10^8 edges -here; at ~10^9 the question is streaming-vs-out-of-core-vs-cluster, which we will -measure rather than assert. - -Why this matters ----------------- - -Most graph work in a notebook or a pipeline is single-node and latency -sensitive: filter to a subgraph, expand a few hops, score it. For that regime, -standing up or paying for a Spark cluster is the wrong shape — the per-query -scheduling and serialization cost swamps the actual work. GFQL runs the same -queries in-process on your dataframe, on CPU, and wins by 1.3–43x here -(most cells 2x+; the closest is Orkut's heavy 2-hop at 1.3x). - -When the workload shifts to whole-graph analytics like PageRank, the GPU engine -(``engine="polars-gpu"``, cugraph) is the tool that beats Spark — by ~10–15x -(14.7x on LiveJournal, 10.5x on Orkut) — on the same single node. The CPU -engine's PageRank is a convenience for when no GPU is present, not a performance -claim. - -**When to go back to Spark.** These *in-memory* numbers hold while the graph and -its intermediates fit in one machine's memory (here, 119 GB unified host/GPU -memory comfortably holds Orkut's 117M edges). Above that, GFQL has two moves -before a cluster: Polars' **streaming engine** (``GFQL_POLARS_CPU_STREAMING=1``, -disk-spill) and the **cudf-polars streaming executor** -(``GFQL_POLARS_GPU_EXECUTOR=streaming``, larger-than-device-memory) — both -opt-in, both untested at 1.8B here (see the Friendster section). A managed Spark -cluster is the right tool when the data already lives there, or when the graph -outgrows even streaming on one node. This page measures the in-memory single-node -regime; it does not claim GFQL replaces a cluster at every scale, nor that -one node is a hard ceiling. - -Fairness and caveats (documented, not hidden) ---------------------------------------------- - -We benchmark the single-node regime where GFQL lives, and we flag every place -that favors or disfavors either side: - -- **local[*] is Spark's single-node configuration.** This measures single-box - multicore, not a distributed cluster. A real cluster amortizes scheduling and - shuffle overhead across many machines and would change the trade-off, - especially at larger scales. We are explicitly benchmarking single-node - latency, which is where GFQL is designed to run. -- **End-to-end materialization on both sides.** Spark is lazy, so every task - ends in a materializing action (``.count()`` / ``.vertices.count()``) to force - honest end-to-end timing. GFQL likewise materializes via - ``len(_nodes)`` / ``len(_edges)``. Both are timed to a real answer, not a lazy - plan. -- **The pandas→polars conversion is charged to GFQL.** GFQL holds edges as - pandas and converts to polars *inside* the timed region on each call. This is - conservative — it counts against GFQL — and is left in deliberately rather - than pre-converting. -- **PageRank convergence differs (disclosed).** GraphFrames runs a fixed - ``maxIter=20``; igraph iterates to ``eps=1e-3`` and cugraph to ``tol=1e-5``. - The comparison is wall-clock-to-usable-scores; we verify all three return the - identical vertex set and rank it identically (LiveJournal: pairwise Spearman - rho = 1.00, top-100 overlap 100/100 — ``bench_graphframes_pagerank_parity.json``), - not per-iteration cost — the algorithms converge to the same ranking at - different cost. -- **In-memory by default (streaming is opt-in).** These results are the default - *in-memory* configuration, which assumes the graph fits in one node's RAM — the - regime this page measures. GFQL does **not** shard across machines, but it *can* - spill to disk / stream: Polars' streaming engine - (``GFQL_POLARS_CPU_STREAMING=1``) and the cudf-polars streaming executor - (``GFQL_POLARS_GPU_EXECUTOR=streaming``) are larger-than-memory paths, off by - default and not exercised in these numbers. We report the host memory (119 GB) - so the in-memory envelope is explicit. -- **Runs are blocked, not interleaved.** On this shared box, GFQL and - GraphFrames were run in separate blocks (all GFQL cells, then all GraphFrames - cells), not interleaved, and only medians are retained per cell. Validation and - final medians agreed within run-to-run noise; we report medians, consistent - with :doc:`benchmark_filter_pagerank`. -- **Warmups and median.** 2 warmups absorb one-time costs (JIT, lazy-plan - compilation, JVM class-loading, executor spin-up, filesystem cache priming) so - the timed runs measure steady state. Median of 5 (not mean) is robust to the - occasional GC / stop-the-world spike on a shared box. Cold load (ETL) is timed - separately, once — a different question from warm query latency. -- **Guardrails.** Each (system, task) is wrapped: an error/OOM records a status - and the matrix continues; missing pyspark/graphframes/GPU is skipped with a - message, never aborting the run. - -Reproducibility ---------------- - -Results are rendered from saved JSON (``_static/graphframes/results.json``) — -this page does **not** rerun benchmarks. The committed harness is -``benchmarks/gfql/bench_graphframes.py`` (design notes in -``benchmarks/gfql/bench_graphframes_DESIGN.md``). To reproduce the LiveJournal -matrix (from ``benchmarks/gfql/``, with the graphframes jar on the Spark -classpath via ``GRAPHFRAMES_JAR``): + :widths: 20 20 20 20 20 + + * - Graph + - filter + - 1-hop + - 2-hop + - PageRank + * - LiveJournal + - 403,561 + - 119,877 + - 1,378,430 + - 3,997,962 + * - Orkut + - 308,666 + - 434,973 + - 1,991,366 + - 3,072,441 + +Which engine to use +------------------- + +- **Filter and traversal**: use GFQL on CPU. The GPU changes these times + little, because at these result sizes data movement, not compute, sets the + floor. Spark's per-query scheduling and shuffle cost dominates its time. +- **Whole-graph PageRank**: use GFQL on GPU (``engine="polars-gpu"``, cugraph). +- **PageRank without a GPU**: GFQL routes the CPU path through single-threaded + igraph. It is 3x to 4x slower than GraphFrames at these sizes, and the gap + grows with graph size. Use it for convenience, not for speed. +- **Larger than one node's memory**: see :ref:`graphframes-friendster`. + +The tasks +--------- + +**filter**: keep nodes with ``degree >= threshold``. SNAP graphs have no +attributes, so both systems compute ``degree`` during cold load. The load time +carries that cost, not the query. The shared threshold makes the filter +identical across systems. + +.. doc-test: skip + +.. code-block:: python + + # GFQL + from graphistry import n + from graphistry.compute.predicates.numeric import ge + g.gfql([n(filter_dict={'degree': ge(42)})], engine="polars") # or "polars-gpu" + + # GraphFrames + gf.degrees.filter("degree >= 42").count() + +**1-hop** and **2-hop**: undirected expansion from a fixed set of 50 +high-degree seed nodes. + +.. doc-test: skip + +.. code-block:: python + + # GFQL + from graphistry import n, e_undirected + g.gfql([n(filter_dict={'id': is_in(seeds)}), e_undirected(hops=1), n()], engine="polars") + +GraphFrames has no k-hop primitive. Its ``bfs`` finds shortest paths between +predicates and ``find`` matches a fixed motif. The Spark side therefore expands +with one iterated undirected edge join per hop and ends in ``.count()``. + +**PageRank**: full graph, damping 0.85. GFQL CPU calls +``g.compute_igraph('pagerank')``. GFQL GPU calls +``g.compute_cugraph('pagerank')``. GraphFrames calls +``gf.pageRank(resetProbability=0.15, maxIter=20)``. The three engines return +the same vertex set and the same ranking. On LiveJournal, pairwise Spearman rho +is 1.00 and the top-100 overlap is 100 of 100 +(``_static/graphframes/bench_graphframes_pagerank_parity.json``). + +.. _graphframes-friendster: + +Friendster (1.8B edges): not measured +------------------------------------- + +Friendster has 1,806,067,135 edges and 65,608,366 nodes +(`SNAP `_). No system +completed a task on the test node (about 120 GB unified memory): + +- **GFQL polars (CPU)**: the harness loads the full edge list into a pandas + frame (about 29 GB) and makes a second pass for degrees. This exceeds physical + RAM before the query runs. +- **GFQL polars-gpu (GPU)**: a direct cudf edge read also exceeds the roughly 120 GB + unified memory pool. +- **GraphFrames (local[*])**: a 90 GB driver heap swaps and does not finish in + usable time. + +This is a limit of the eager in-memory harness, not a measured engine limit. +GFQL has two opt-in larger-than-memory paths that this benchmark did not use. +``GFQL_POLARS_CPU_STREAMING=1`` selects the Polars streaming engine, which +spills to disk. ``GFQL_POLARS_GPU_EXECUTOR=streaming`` selects the cudf-polars +streaming executor for results larger than device memory. Both need a lazy +source such as ``pl.scan_parquet`` instead of an eager ``pandas.read_parquet``. +Measuring that path at 1.8B edges is follow-up work. + +.. _graphframes-method: + +Method and limits +----------------- + +- **Scope**: single node, in memory. ``local[*]`` is Spark's single-node mode. + A cluster amortizes scheduling and shuffle cost across machines and changes + the trade-off at larger scale. Use a Spark cluster when the data already + lives there or the graph exceeds one node's memory, including the streaming + paths above. +- **Timing**: median of 5 runs after 2 warmups per cell. LiveJournal GPU + PageRank is median of 3 after 1 warmup, rerun after a transient GPU fault. + Cold load is timed once, separately. +- **Materialization**: Spark is lazy, so every task ends in ``.count()`` or + ``.vertices.count()``. GFQL materializes with ``len(_nodes)`` or + ``len(_edges)``. +- **Conversion cost**: GFQL holds edges as pandas and converts to Polars inside + the timed region on each call. This counts against GFQL. +- **PageRank convergence**: GraphFrames runs a fixed ``maxIter=20``. igraph + stops at ``eps=1e-3`` and cugraph at ``tol=1e-5``. Times compare + wall-clock to a usable ranking; the rankings agree as shown above. +- **Result parity**: each task returns the same result size on all systems + (table above). A mismatch is treated as a bug, not a result. +- **Run order**: all GFQL cells ran in one block, then all GraphFrames cells, + on a shared machine. Only medians are kept. +- **Errors**: each (system, task) cell records a status on error or OOM and the + matrix continues. Missing pyspark, graphframes, or GPU skips the cell. +- **Environment**: ``dgx-spark`` (GB10 GPU, about 120 GB unified memory); + GraphFrames ``0.8.4-spark3.5-s_2.12``; PySpark ``3.5.1``. + +Reproduce +--------- + +This page renders saved results from ``_static/graphframes/results.json``. The +harness is ``benchmarks/gfql/bench_graphframes.py`` (design notes in +``benchmarks/gfql/bench_graphframes_DESIGN.md``). From ``benchmarks/gfql/``, +with the GraphFrames jar on the Spark classpath via ``GRAPHFRAMES_JAR``: .. code-block:: bash @@ -386,28 +237,12 @@ classpath via ``GRAPHFRAMES_JAR``): --tasks filter,hop1,hop2,pagerank \ --filter-threshold 42 --warmups 2 --iters 5 -Orkut uses ``--dataset orkut --filter-threshold 162``. The shared -``--filter-threshold`` makes the filter task bit-identical across systems. - -Environment ------------ - -- Host: ``dgx-spark``, single node; GPU: ``GB10`` -- GFQL engines: ``engine="polars"`` (CPU, PageRank via igraph) and - ``engine="polars-gpu"`` (GPU, PageRank via cugraph) -- Spark: GraphFrames ``0.8.4-spark3.5-s_2.12``, PySpark ``3.5.1``, ``local[*]`` -- Datasets: `SNAP `_ LiveJournal (~35M edges), - Orkut (~117M edges), Friendster (~1.8B edges, stretch) -- Measurement: median of 5 runs after 2 warmups; result-size parity enforced - per task; results rendered from saved JSON +Orkut uses ``--dataset orkut --filter-threshold 162``. See also -------- -- :doc:`engines` — choosing an engine; four-engine and external-tool comparison - (including where PuppyGraph / warehouse-federated tools fit — not yet - benchmarked head-to-head) -- :doc:`benchmark_filter_pagerank` — GFQL CPU/GPU vs Neo4j + GDS -- :doc:`cypher` — Cypher syntax through ``g.gfql("MATCH ...")`` -- :doc:`overview` — GFQL design, features, and GPU acceleration -- :doc:`about` — 10-minute introduction to GFQL +- :doc:`engines`: choosing pandas, Polars, cuDF, or Polars-GPU +- :doc:`benchmark_filter_pagerank`: GFQL CPU/GPU vs Neo4j + GDS +- :doc:`cypher`: Cypher syntax through ``g.gfql("MATCH ...")`` +- :doc:`overview`: GFQL design and features diff --git a/docs/source/gfql/index.rst b/docs/source/gfql/index.rst index 68b4f044d2..f8711666ae 100644 --- a/docs/source/gfql/index.rst +++ b/docs/source/gfql/index.rst @@ -1,76 +1,42 @@ GFQL: The Dataframe-Native Graph Query Language =============================================== -Welcome to **GFQL**, the first fully vectorized dataframe-native graph query -language with an open-source GPU runtime. GFQL is part of the -**PyGraphistry** ecosystem and is designed to make graph analytics easier and -faster without requiring a graph database as the execution layer. Whether -you're working with **CPUs** or leveraging **GPU acceleration** for massive -datasets, GFQL integrates directly into Python dataframe workflows through a -simple `pip install graphistry`. - -**GFQL bridges the gap** between traditional storage-tier graph databases and -the modern compute tier, allowing you to perform high-performance graph queries -directly on your dataframes. It is built to feel familiar to users of Cypher, -other graph query languages, and popular dataframe libraries. By being native -to accelerated Python data-science technologies such as Apache Arrow, NumPy, -NVIDIA RAPIDS, and Graphistry, it can already handle workloads like 100M+ edges -in interactive time on a single machine. - -If you are new to Cypher: Cypher is a graph query language popularized by -Neo4j and related tools. It uses ASCII-art graph patterns such as -``(n1)-[e1]->(n2)`` to describe traversals from one node to another across an -edge. GFQL supports a bounded Cypher surface directly through -``g.gfql("MATCH ...")``, so Cypher users can keep familiar ``MATCH`` / -``WHERE`` / ``RETURN`` patterns while moving execution onto GFQL's vectorized -columnar engine and open-source GPU runtime. Use ``g.gfql_remote([...])`` when -you want the same GFQL model executed remotely. - -For Cypher syntax through ``g.gfql("MATCH ...")``, start with -:doc:`Cypher Syntax In GFQL `, -:doc:`GFQL Quick Reference `, -:doc:`GFQL RETURN `, -and :doc:`Cypher to GFQL Mapping `. - -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'``, faster than pandas on all nine queries of the q1-q9 Cypher suite) -- 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) instead of O(E), so cost tracks the seeds, not the graph) -- Translating existing Cypher to native GFQL: :doc:`spec/cypher_mapping` -- Building agents/integrations: :doc:`spec/language` + :doc:`spec/python_embedding` + :doc:`spec/wire_protocol` - - -See also: +GFQL is a graph query language that runs directly on Python dataframes. It +needs no graph database. The same query runs on pandas, Polars, cuDF (GPU), or +Polars-GPU, and on a remote Graphistry server. Install it with +``pip install graphistry``. + +GFQL accepts two syntaxes. The Python chain syntax composes ``n()`` and ``e()`` +steps. The Cypher syntax, ``g.gfql("MATCH (a)-[e]->(b) ...")``, covers a +bounded subset of Cypher, the graph query language popularized by Neo4j. Both +compile to vectorized dataframe operations, so one machine handles graphs of +100M+ edges in interactive time (see :doc:`benchmark_graphframes`). + +Where to start +-------------- + +- **New to GFQL**: :doc:`about` (10 minutes), then :doc:`quick`. +- **Coming from Cypher**: :doc:`cypher`, then :doc:`spec/cypher_mapping`. +- **Need speed**: :doc:`engines` picks the engine. ``engine='polars'`` is the + one-keyword CPU speedup; :doc:`performance` covers GPU and remote GPU. +- **Start from known nodes**: :doc:`indexing` and :doc:`index_adjacency` make + seeded lookups cost O(degree) instead of O(E). .. toctree:: :maxdepth: 1 - :caption: User Guide + :caption: Start Here about overview - remote - Choosing an Engine - Pay-As-You-Go Resident Indexing - Seeded Traversal Indexes - GFQL CPU & GPU Acceleration - End-to-End Benchmark - vs Spark GraphFrames - translate - combo quick cypher - where - return - predicates/quick - datetime_filtering - builtin_calls - policy - strict_mode - schema - wire_protocol_examples + +.. toctree:: + :maxdepth: 1 + :caption: Guides + + Performance and Benchmarks + Language Reference .. toctree:: :maxdepth: 2 diff --git a/docs/source/gfql/perf/index.rst b/docs/source/gfql/perf/index.rst new file mode 100644 index 0000000000..b3ce3e760a --- /dev/null +++ b/docs/source/gfql/perf/index.rst @@ -0,0 +1,24 @@ +Performance and Benchmarks +========================== + +How to make GFQL fast, and what it measures against other systems. + +Start with :doc:`../engines` to pick pandas, Polars, cuDF, or Polars-GPU. +:doc:`../performance` covers vectorization, GPU acceleration, and measured +engine comparisons. :doc:`../indexing` and :doc:`../index_adjacency` speed up +queries that start from known nodes. :doc:`../remote` runs the same queries on +a Graphistry server GPU. + +The two benchmark pages compare GFQL with Neo4j + GDS and with Apache Spark +GraphFrames on one machine. + +.. toctree:: + :maxdepth: 1 + + Choosing an Engine <../engines> + CPU and GPU Acceleration <../performance> + Pay-As-You-Go Resident Indexing <../indexing> + Seeded Traversal Indexes <../index_adjacency> + ../remote + Benchmark: Filter + PageRank vs Neo4j + GDS <../benchmark_filter_pagerank> + Benchmark: vs Spark GraphFrames <../benchmark_graphframes> diff --git a/docs/source/gfql/reference/index.rst b/docs/source/gfql/reference/index.rst new file mode 100644 index 0000000000..e2c0ec21f8 --- /dev/null +++ b/docs/source/gfql/reference/index.rst @@ -0,0 +1,24 @@ +Language Reference +================== + +Reference pages for the GFQL query language: filtering, projection, +predicates, built-in calls, policies, validation modes, and wire formats. + +For a guided introduction, see :doc:`../about`. For the formal specification, +see :doc:`../spec/index`. + +.. toctree:: + :maxdepth: 1 + + ../where + ../return + ../predicates/quick + ../datetime_filtering + ../builtin_calls + ../combo + ../translate + ../policy + ../strict_mode + ../schema + ../wire_protocol_examples + ../loading_graph_data From be81baf054d88273fa74277fe628349b412aa2c4 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 17:05:45 -0700 Subject: [PATCH 02/29] docs(gfql): owner review wave: case study in Start Here, charts, one provenance block - overview: positioning paragraph (only open-source in-process Cypher on dataframes; columnar/vectorized framing); remove empty hidden toctree that rendered the page as a folder; trim marketing wording - benchmark_filter_pagerank: retitle as a case study, list it under Start Here; lede says what is compared and the outcome; drop the above-the-fold caveat paragraph; add a pipeline lead sentence - benchmark_graphframes: lede says what is compared and the 7-of-8 outcome; add per-task bar charts rendered from results.json by gfql_bench_charts.py (byte-reproducible, covered by the chart sync test) - _ext/gfql_bench: bench-provenance accepts several runs and renders one Measurement block (reader-facing fields only; caveats folded in via :disclosures:) - vendor pyg-bench published numbers (runtime strings now name the RAPIDS base image; pyg-bench #220); numbers unchanged Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 3 +- docs/source/_data/gfql_benchmarks.json | 14 +- docs/source/_ext/gfql_bench.py | 128 ++++++++++++---- docs/source/_ext/gfql_bench_charts.py | 144 +++++++++++++++++- .../_static/graphframes/livejournal_tasks.svg | 53 +++++++ .../gfql/_static/graphframes/orkut_tasks.svg | 53 +++++++ .../source/gfql/benchmark_filter_pagerank.rst | 47 +++--- docs/source/gfql/benchmark_graphframes.rst | 36 +++-- docs/source/gfql/index.rst | 4 +- docs/source/gfql/overview.rst | 15 +- docs/source/gfql/perf/index.rst | 7 +- docs/test_bench_numbers.py | 2 +- 12 files changed, 415 insertions(+), 91 deletions(-) create mode 100644 docs/source/gfql/_static/graphframes/livejournal_tasks.svg create mode 100644 docs/source/gfql/_static/graphframes/orkut_tasks.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index b84891dc62..9202863519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Docs - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. -- **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` and `gfql/benchmark_graphframes` open with the measured outcome and the engine to use, keep every number unchanged, and move measurement rules into one "Method and limits" section per page. GraphFrames literals were re-verified against `gfql/_static/graphframes/results.json`; the filter/PageRank page still prints from the vendored pyg-bench artifact. +- **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome, keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). +- **GFQL overview states the positioning up front**: `gfql/overview` says GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone. ## [0.59.0 - 2026-08-31] diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 5d5cd50c0c..0d33a461e1 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -912,8 +912,8 @@ } }, "contract_version": 3, - "generated_at": "2026-08-30T17:33:55Z", - "generated_by": "graphistry/pyg-bench 1ccda07c scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T00:02:28Z", + "generated_by": "graphistry/pyg-bench c0e15bb2 scripts/export_docs_numbers.py", "policy": { "literal_allowlist": {}, "managed_docs": [ @@ -948,7 +948,7 @@ "quiet_host": true, "reps": "graph loaded once, then 2 warmups + 5 timed runs per arm on the resident graph; median", "row_validation": "Every arm records the node id set its pipeline selected, captured outside the timed region. Comparability is the Jaccard index of those sets against a 0.95 threshold declared before the run: Twitter CPU/GPU 0.991, CPU/Neo4j 0.974, GPU/Neo4j 0.972; GPlus CPU/GPU 0.951.", - "runtime": "graphistry/test-rapids-official:26.02-gfql-polars with python-igraph 1.0.0; cuDF 26.2.1, cuGraph 26.2.0, pandas 2.3.3; Neo4j 2026.02.2 + graph-data-science in Docker on the same host" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13) with python-igraph 1.0.0; cuDF 26.2.1, cuGraph 26.2.0, pandas 2.3.3; Neo4j 2026.02.2 + graph-data-science in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "filter-pagerank-gplus-locked-20260830": { "artifact": "results/gplus-locked-baseline-20260830", @@ -963,7 +963,7 @@ "quiet_host": true, "reps": "12 position-balanced slots, six per arm; each slot 2 warmups + 11 timed runs; median of slot medians", "row_validation": "All 12 selected-node sets had Jaccard 1.0 against the reference (gate 0.95); every slot passed typed result, exact run-contract, and load/self-spike validation; the committed aggregate exactly recomputes from all slots.", - "runtime": "graphistry/test-rapids-official:26.02-gfql-polars; GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13); GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "graphbench-q1q9-100k-20260726": { "artifact": "results/graphbench-matched-q1q9-20260726/gb100k", @@ -978,7 +978,7 @@ "quiet_host": true, "reps": "2 warmups + 5 timed runs per slot; slot median, then median across 4 position-balanced slots (K G G K K G G K)", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is only publishable when its rows equal Kuzu's on every slot", - "runtime": "graphistry/test-rapids-official:26.02-gfql-polars, polars 1.35.2; Kuzu in a host venv on CPU" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu in a host venv on CPU; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "graphbench-q1q9-20k-20260726": { "artifact": "results/graphbench-matched-q1q9-20260726/gb20k", @@ -993,7 +993,7 @@ "quiet_host": true, "reps": "3 warmups + 7 timed runs per slot; slot median, then median across 4 position-balanced slots (K G I I G K K G I I G K)", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is only publishable when its rows equal Kuzu's on every slot", - "runtime": "graphistry/test-rapids-official:26.02-gfql-polars, polars 1.35.2; Kuzu in a host venv on CPU" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu in a host venv on CPU; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "snb-is3-single-query-20260727": { "artifact": "results/is3-single-query-r{1,2,3} and results/is3-twopass-base-r{1,2,3}", @@ -1008,7 +1008,7 @@ "quiet_host": true, "reps": "3 repetitions of configs/suites/ab-is1-is7-sf1-polars.yaml, each already a median of 5 measured repeats after 1 warmup; median across repetitions", "row_validation": "920/920 rows, value-identical to the independently derived expected fixture in all 3 repetitions", - "runtime": "graphistry/test-rapids-official:26.02-gfql-polars, polars 1.35.2" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" } }, "schema_version": 1 diff --git a/docs/source/_ext/gfql_bench.py b/docs/source/_ext/gfql_bench.py index 48c698d2a0..82b595b657 100644 --- a/docs/source/_ext/gfql_bench.py +++ b/docs/source/_ext/gfql_bench.py @@ -25,7 +25,7 @@ from typing import Dict, List, Optional, Tuple from docutils import nodes -from docutils.parsers.rst import Directive +from docutils.parsers.rst import Directive, directives from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment @@ -79,11 +79,20 @@ def role(name: str, rawtext: str, key: str, lineno: int, inliner: Inliner, class BenchProvenance(Directive): - """Render the run record behind the numbers on this page.""" + """Render the run records behind the numbers on this page as one block. + + Several run ids may be given; the block then keeps only the fields a reader compares + across runs. A field whose value is the same in every run is shown once, and a field + that differs is shown per run, keyed by that run's measurement date. The + ``:disclosures:`` flag folds every disclosure attached to a number this page prints + into the same block, in place of a separate ``bench-disclosures``. + """ required_arguments = 1 - optional_arguments = 0 + optional_arguments = 8 + final_argument_whitespace = False has_content = False + option_spec = {'disclosures': directives.flag} FIELDS = [ ('measured_at', 'Measured'), @@ -97,19 +106,37 @@ class BenchProvenance(Directive): ('row_validation', 'Result validation'), ('competitor_version', 'Competitor version'), ] + MERGED_FIELDS = [ + ('measured_at', 'Measured'), + ('host', 'Host'), + ('reps', 'Repetitions'), + ('runtime', 'Runtime'), + ('dataset', 'Dataset'), + ('row_validation', 'Result validation'), + ] def run(self) -> List[nodes.Node]: state = _state() docname = self.state.document.settings.env.docname - run_id = self.arguments[0].strip() - state.provenance.setdefault(docname, []).append(run_id) - run = state.run(run_id) - if run is None: - message = '{}: no run {!r} in the published artifact'.format(docname, run_id) - state.fail(message) - logger.warning('[gfql-bench] %s', message) - return [] - return [_admonition('Measurement', _fields(run, self.FIELDS))] + runs = [] # type: List[JSONObject] + for argument in self.arguments: + run_id = argument.strip() + state.provenance.setdefault(docname, []).append(run_id) + run = state.run(run_id) + if run is None: + message = '{}: no run {!r} in the published artifact'.format(docname, run_id) + state.fail(message) + logger.warning('[gfql-bench] %s', message) + return [] + runs.append(run) + spec = self.FIELDS if len(runs) == 1 else self.MERGED_FIELDS + field_list = _merged_fields(runs, spec) + if 'disclosures' in self.options: + state.disclosed.append(docname) + disclosures = _disclosures(state, docname) + if disclosures: + field_list += _field('Caveats', _bullets(disclosures)) + return [_admonition('Measurement', field_list)] class BenchDisclosures(Directive): @@ -123,26 +150,67 @@ def run(self) -> List[nodes.Node]: state = _state() docname = self.state.document.settings.env.docname state.disclosed.append(docname) - - seen = [] # type: List[str] - for key in state.refs.get(docname, []): - cell = state.cell(key) - if cell is None: - continue - raw = cell.get('disclosures') - if not isinstance(raw, list): - continue - for item in raw: - if isinstance(item, str) and item and item not in seen: - seen.append(item) + seen = _disclosures(state, docname) if not seen: return [] - bullets = nodes.bullet_list() - for item in seen: - entry = nodes.list_item() - entry += nodes.paragraph(text=item) - bullets += entry - return [_admonition('About these measurements', bullets)] + return [_admonition('About these measurements', _bullets(seen))] + + +def _disclosures(state: State, docname: str) -> List[str]: + seen = [] # type: List[str] + for key in state.refs.get(docname, []): + cell = state.cell(key) + if cell is None: + continue + raw = cell.get('disclosures') + if not isinstance(raw, list): + continue + for item in raw: + if isinstance(item, str) and item and item not in seen: + seen.append(item) + return seen + + +def _bullets(items: List[str]) -> nodes.bullet_list: + bullets = nodes.bullet_list() + for item in items: + entry = nodes.list_item() + entry += nodes.paragraph(text=item) + bullets += entry + return bullets + + +def _field(label: str, body_content: nodes.Element) -> nodes.field: + field = nodes.field() + field += nodes.field_name(text=label) + body = nodes.field_body() + body += body_content + field += body + return field + + +def _merged_fields(runs: List[JSONObject], spec: List[Tuple[str, str]]) -> nodes.field_list: + field_list = nodes.field_list() + for key, label in spec: + values = [] # type: List[Tuple[str, str]] + for run in runs: + value = run.get(key) + if isinstance(value, str) and value: + values.append((str(run.get('measured_at', '')), value)) + if not values: + continue + distinct = [] # type: List[str] + for _, value in values: + if value not in distinct: + distinct.append(value) + if len(distinct) == 1: + field_list += _field(label, nodes.paragraph(text=distinct[0])) + elif key == 'measured_at': + field_list += _field(label, nodes.paragraph(text=' and '.join(distinct))) + else: + field_list += _field(label, _bullets( + ['{}: {}'.format(date, value) for date, value in values])) + return field_list def _fields(run: JSONObject, spec: List[Tuple[str, str]]) -> nodes.field_list: diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index 044959e051..8237df340a 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -7,6 +7,9 @@ committed file differs, so a chart cannot go on asserting a number the artifact no longer publishes - which is exactly how withdrawn figures survived on this page as glyph paths. +The GraphFrames task charts render the same way from +``docs/source/gfql/_static/graphframes/results.json``. + Regenerate after vendoring a new artifact:: python3 docs/source/_ext/gfql_bench_charts.py --write @@ -32,6 +35,12 @@ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'gfql', '_static', 'filter_pagerank') +#: The GraphFrames comparison renders from its own saved results, next to its charts. +GRAPHFRAMES_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'gfql', '_static', 'graphframes') +GRAPHFRAMES_RESULTS_JSON = os.path.join(GRAPHFRAMES_DIR, 'results.json') + WIDTH = 720 PAD = 16 HEADER_H = 74 @@ -200,25 +209,144 @@ def render(name: str, payload: JSONObject) -> str: return '\n'.join(out) + '\n' + +#: GraphFrames task charts: one per dataset, four task groups, three systems per group. +#: Bars are scaled per task (the slowest system fills the row) because task times span +#: five orders of magnitude; every bar prints its own value, and every GFQL bar prints +#: its own ratio against GraphFrames, so no visual comparison is made across tasks. +GF_LABEL_W = 178 +GF_BAR_MAX = 300 +GF_GROUP_HEAD = 24 +GF_BAR_ROW = 28 +GF_BAR_H = 18 +GF_GROUP_GAP = 8 +GF_TASKS = ( + ('filter', 'filter: degree >= {threshold}'), + ('hop1', '1-hop from 50 seeds'), + ('hop2', '2-hop from 50 seeds'), + ('pagerank', 'PageRank, full graph'), +) +GF_SYSTEMS = ( + ('gfql-polars', 'GFQL polars (CPU)', 'cpu'), + ('gfql-polars-gpu', 'GFQL polars-gpu (GPU)', 'gpu'), + ('graphframes', 'GraphFrames local[*]', 'neo'), +) +GF_CHARTS: dict[str, tuple[str, str, int]] = OrderedDict(( + ('livejournal_tasks.svg', ('lj', 'LiveJournal', 42)), + ('orkut_tasks.svg', ('orkut', 'Orkut', 162)), +)) + + +def _gf_time(task: str, ms: float) -> str: + if task == 'pagerank': + seconds = ms / 1000.0 + return '{:.2f}s'.format(seconds) if seconds < 10 else '{:.1f}s'.format(seconds) + return '{:.1f}ms'.format(ms) + + +def _gf_ratio(system_ms: float, graphframes_ms: float) -> str: + ratio = graphframes_ms / system_ms + if ratio >= 1: + return '{:.1f}x faster'.format(ratio) + return '{:.2f}x (slower)'.format(ratio) + + +def _gf_dataset(results: JSONObject, key: str) -> JSONObject: + dataset = results.get(key) + if not isinstance(dataset, dict): + raise ChartError('{} has no dataset {!r}'.format( + os.path.basename(GRAPHFRAMES_RESULTS_JSON), key)) + return dataset + + +def _gf_median_ms(dataset: JSONObject, task: str, system: str) -> float: + tasks = dataset.get('tasks') + arms = tasks.get(task) if isinstance(tasks, dict) else None + arm = arms.get(system) if isinstance(arms, dict) else None + value = arm.get('median_ms') if isinstance(arm, dict) else None + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ChartError('{}.{}.{} has no median_ms'.format( + dataset.get('name', '?'), task, system)) + return float(value) + + +def render_graphframes(name: str, results: JSONObject) -> str: + """Render one GraphFrames task chart to SVG text.""" + key, label, threshold = GF_CHARTS[name] + dataset = _gf_dataset(results, key) + edges = dataset.get('n_edges') + nodes_ = dataset.get('n_nodes') + title = '{}: {:,} nodes / {:,} edges'.format(label, int(nodes_), int(edges)) + subtitle = 'Median of 5 after 2 warmups; same result size on every system. Lower is better.' + group_h = GF_GROUP_HEAD + GF_BAR_ROW * len(GF_SYSTEMS) + GF_GROUP_GAP + height = HEADER_H + group_h * len(GF_TASKS) + FOOT_H + out = [ + ''.format(WIDTH, height, WIDTH, height, _esc(title)), + '{}'.format(_esc(title)), + ''.format(STYLE), + '', + ''.format(WIDTH, height), + '{}'.format(PAD, _esc(title)), + '{}'.format(PAD, _esc(subtitle)), + ] + bar_x = PAD + GF_LABEL_W + for group_index, (task, task_label) in enumerate(GF_TASKS): + group_top = HEADER_H + group_index * group_h + out.append('{}'.format( + PAD, group_top + 15, _esc(task_label.format(threshold=threshold)))) + times = {system: _gf_median_ms(dataset, task, system) for system, _, _ in GF_SYSTEMS} + slowest = max(times.values()) + graphframes_ms = times['graphframes'] + out.append(''.format( + bar_x - 1, group_top + GF_GROUP_HEAD - 2, GF_BAR_ROW * len(GF_SYSTEMS))) + for row_index, (system, system_label, tone) in enumerate(GF_SYSTEMS): + row_top = group_top + GF_GROUP_HEAD + row_index * GF_BAR_ROW + bar_top = row_top + (GF_BAR_ROW - GF_BAR_H) / 2 + out.append('{}'.format( + PAD + 10, row_top + 18, _esc(system_label))) + width = max(MIN_BAR, GF_BAR_MAX * times[system] / slowest) + out.append(''.format( + tone, _bar_path(bar_x, bar_top, width, GF_BAR_H))) + spans = ['{}'.format(_esc(_gf_time(task, times[system])))] + if system != 'graphframes': + spans.append('{}'.format( + _esc(_gf_ratio(times[system], graphframes_ms)))) + out.append('{}'.format( + _num(bar_x + width + 8), row_top + 18, ''.join(spans))) + out.append('{}'.format( + PAD, height - 11, + _esc('Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores.'))) + out.append('') + return '\n'.join(out) + '\n' + + +def chart_path(name: str) -> str: + """Where the committed copy of a chart lives.""" + if name in GF_CHARTS: + return os.path.join(GRAPHFRAMES_DIR, name) + return os.path.join(CHART_DIR, name) + + def rendered(payload: JSONObject | None = None) -> dict[str, str]: """Every chart, keyed by file name, rendered from the vendored artifact.""" data = payload if payload is not None else load(BENCHMARKS_JSON) - return OrderedDict((name, render(name, data)) for name in CHARTS) + charts = OrderedDict((name, render(name, data)) for name in CHARTS) + results = load(GRAPHFRAMES_RESULTS_JSON) + for name in GF_CHARTS: + charts[name] = render_graphframes(name, results) + return charts def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--out-dir', default=CHART_DIR) parser.add_argument('--write', action='store_true', help='rewrite the charts; without it, only report what is stale') args = parser.parse_args(argv) - if args.write: - os.makedirs(args.out_dir, exist_ok=True) - stale: list[str] = [] for name, svg in rendered().items(): - path = os.path.join(args.out_dir, name) + path = chart_path(name) current: str | None = None if os.path.exists(path): with open(path, encoding='utf-8') as handle: @@ -227,6 +355,7 @@ def main(argv: Sequence[str] | None = None) -> int: continue stale.append(name) if args.write: + os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w', encoding='utf-8') as handle: handle.write(svg) print('[wrote] {}'.format(path)) @@ -234,7 +363,8 @@ def main(argv: Sequence[str] | None = None) -> int: return 0 for name in stale: print('[stale] {}'.format(name)) - print('{} of {} charts differ from the published numbers'.format(len(stale), len(CHARTS))) + print('{} of {} charts differ from the published numbers'.format( + len(stale), len(CHARTS) + len(GF_CHARTS))) return 1 if stale else 0 diff --git a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg new file mode 100644 index 0000000000..c158854a23 --- /dev/null +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -0,0 +1,53 @@ + +LiveJournal: 3,997,962 nodes / 34,681,189 edges + + + +LiveJournal: 3,997,962 nodes / 34,681,189 edges +Median of 5 after 2 warmups; same result size on every system. Lower is better. +filter: degree >= 42 + +GFQL polars (CPU) + +2.1ms43.0x faster +GFQL polars-gpu (GPU) + +2.4ms37.7x faster +GraphFrames local[*] + +90.4ms +1-hop from 50 seeds + +GFQL polars (CPU) + +236.8ms6.0x faster +GFQL polars-gpu (GPU) + +191.4ms7.4x faster +GraphFrames local[*] + +1421.7ms +2-hop from 50 seeds + +GFQL polars (CPU) + +1669.3ms2.1x faster +GFQL polars-gpu (GPU) + +1542.1ms2.3x faster +GraphFrames local[*] + +3583.3ms +PageRank, full graph + +GFQL polars (CPU) + +49.3s0.33x (slower) +GFQL polars-gpu (GPU) + +1.11s14.7x faster +GraphFrames local[*] + +16.3s +Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores. + diff --git a/docs/source/gfql/_static/graphframes/orkut_tasks.svg b/docs/source/gfql/_static/graphframes/orkut_tasks.svg new file mode 100644 index 0000000000..a89e5b5a81 --- /dev/null +++ b/docs/source/gfql/_static/graphframes/orkut_tasks.svg @@ -0,0 +1,53 @@ + +Orkut: 3,072,441 nodes / 117,185,083 edges + + + +Orkut: 3,072,441 nodes / 117,185,083 edges +Median of 5 after 2 warmups; same result size on every system. Lower is better. +filter: degree >= 162 + +GFQL polars (CPU) + +1.7ms41.5x faster +GFQL polars-gpu (GPU) + +2.0ms35.3x faster +GraphFrames local[*] + +70.6ms +1-hop from 50 seeds + +GFQL polars (CPU) + +562.9ms6.8x faster +GFQL polars-gpu (GPU) + +442.0ms8.7x faster +GraphFrames local[*] + +3826.6ms +2-hop from 50 seeds + +GFQL polars (CPU) + +9439.8ms1.2x faster +GFQL polars-gpu (GPU) + +8860.2ms1.3x faster +GraphFrames local[*] + +11582.9ms +PageRank, full graph + +GFQL polars (CPU) + +160.1s0.23x (slower) +GFQL polars-gpu (GPU) + +3.50s10.5x faster +GraphFrames local[*] + +36.8s +Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores. + diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index 2413543b9e..1960632181 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -1,19 +1,22 @@ -GFQL Cypher Filter + PageRank Benchmark -======================================== +Case Study: Cypher + PageRank Pipeline, GFQL vs Neo4j + GDS +============================================================ .. image:: _static/gfql-mascot.png :alt: GFQL mascot :width: 160px :align: right -One ``g.gfql(...)`` call runs a three-stage pipeline (filter, PageRank, filter) -on a dataframe already loaded in Python. On the 30M-edge GPlus graph, the GFQL -GPU path finishes in :bench:`pagerank.gplus.gfql_gpu` and the GFQL CPU path in -:bench:`pagerank.gplus.gfql_cpu`. The same pipeline on Neo4j + Graph Data -Science (GDS) takes :bench:`pagerank.gplus.neo4j_gds`. Use the GPU engine for -this workload when one is available. It is -:bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster than the CPU engine on -GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. +This case study runs one three-stage graph pipeline, filter, PageRank, filter, +on two systems. GFQL is Graphistry's open-source graph query language: Cypher +that executes in-process on Python dataframes with no database. Neo4j + Graph +Data Science (GDS) is the graph database and its analytics library. On both +graphs, Twitter (2.4M edges) and GPlus (30M edges), GFQL finished the pipeline +faster than Neo4j + GDS, on CPU and on GPU. On GPlus the GFQL GPU path takes +:bench:`pagerank.gplus.gfql_gpu`, the GFQL CPU path +:bench:`pagerank.gplus.gfql_cpu`, and Neo4j + GDS +:bench:`pagerank.gplus.neo4j_gds`. Use the GPU engine when one is available: +it is :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster than the CPU engine +on GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. .. list-table:: :header-rows: 1 @@ -35,10 +38,6 @@ GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. - :bench:`pagerank.gplus.gfql_gpu` - :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` -Each time is the warm end-to-end pipeline. The Neo4j and GFQL columns are -direct pipeline times under different profiles, so the table gives no -GFQL-to-Neo4j ratio. See :ref:`pagerank-method`. - .. image:: _static/filter_pagerank/twitter_pipeline.svg :alt: Twitter warm pipeline time: Neo4j + GDS 11.72s, GFQL Cypher CPU 1.58s, GFQL Cypher GPU 0.24s @@ -48,6 +47,10 @@ GFQL-to-Neo4j ratio. See :ref:`pagerank-method`. The pipeline ------------ +A three-phase graph pipeline: filter, run PageRank, filter again. The query is +standard Cypher extended with GFQL's graph pipeline syntax. Each ``GRAPH { }`` +block takes a graph in and passes a graph on. + .. code-block:: python # pip install graphistry @@ -82,8 +85,8 @@ The query does not change between engines: - **CPU**: ``engine="pandas"``, ``backend="igraph"`` - **GPU**: ``engine="cudf"``, ``backend="cugraph"`` -Each ``GRAPH { }`` block takes a graph as input and returns a graph. Intermediate -graphs stay in Arrow, pandas, or cuDF memory in the same Python process. GFQL +Intermediate graphs stay in Arrow, pandas, or cuDF memory in the same Python +process. GFQL returns the same result on every engine or rejects the query before execution; see :doc:`engines` for the parity rules. @@ -137,12 +140,13 @@ Method and limits node set. - **Timing**: warm runs after warm-up. The GPlus Neo4j time comes from a later locked run of twelve position-balanced slots on one machine; the - provenance blocks below name each run. + Measurement block below records both runs. - **Profiles differ**: GFQL reuses frames already resident in Python. Neo4j includes server round trips, writes marker properties in both filter stages, and rebuilds the GDS in-memory projection on every timed iteration. The Neo4j column is therefore a direct pipeline time, not an engine-primitive - time, and no GFQL-vs-Neo4j ratio is published. + time. The page states which system finished first but publishes no + GFQL-vs-Neo4j ratio. - **Comparable ratio**: the GPU-vs-CPU column compares the same GFQL query and the same profile, so that ratio is published. - **Scope**: for the four-engine CPU/GPU comparison and engine choice, see @@ -157,8 +161,5 @@ Provenance Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, which pyg-bench publishes. -.. bench-provenance:: filter-pagerank-20260728 - -.. bench-provenance:: filter-pagerank-gplus-locked-20260830 - -.. bench-disclosures:: +.. bench-provenance:: filter-pagerank-20260728 filter-pagerank-gplus-locked-20260830 + :disclosures: diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index f9e7fa7174..f59c66f777 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -6,19 +6,29 @@ GFQL vs Apache Spark GraphFrames on One Node :width: 160px :align: right -On one machine, GFQL answers graph filters and k-hop traversals 1.3x to 43x -faster than Apache Spark GraphFrames. The CPU engine alone wins every one of -those cells. For whole-graph PageRank, use the GFQL GPU engine: it is 10x to -15x faster than GraphFrames. The GFQL CPU PageRank path (igraph) is slower -than GraphFrames. - -The workloads are four tasks on two SNAP graphs: LiveJournal (35M edges) and -Orkut (117M edges). GFQL runs with ``engine="polars"`` (CPU) and -``engine="polars-gpu"`` (GPU). GraphFrames runs on Spark ``local[*]``, a -single-node JVM using all cores. Every cell is the median of 5 timed runs -after 2 warmups, and every task returns the same result size on all three -systems. One cell, LiveJournal GPU PageRank, is the median of 3 runs after 1 -warmup. See :ref:`graphframes-method` for the full measurement rules. +This page benchmarks GFQL against Apache Spark GraphFrames on one machine. +GFQL is Graphistry's open-source graph query language: Cypher and Python +chains that run in-process on dataframes, with no database or cluster. +GraphFrames is Spark's graph library, run here on ``local[*]`` (a single-node +JVM using all cores). The workload is four tasks on two graphs, LiveJournal +(35M edges) and Orkut (117M edges). GFQL is faster than GraphFrames in seven of +the eight cells with the CPU engine alone. Graph filters and k-hop traversals +run 1.3x to 43x faster. The exception is whole-graph PageRank on CPU, where +GraphFrames beats GFQL's igraph path. On that task the GFQL GPU engine is 10x +to 15x faster than GraphFrames. Use GFQL on CPU for filters and traversals, +and on GPU for PageRank. + +.. image:: _static/graphframes/livejournal_tasks.svg + :alt: LiveJournal task times: GFQL CPU and GPU versus GraphFrames for filter, 1-hop, 2-hop, and PageRank + +.. image:: _static/graphframes/orkut_tasks.svg + :alt: Orkut task times: GFQL CPU and GPU versus GraphFrames for filter, 1-hop, 2-hop, and PageRank + +GFQL runs with ``engine="polars"`` (CPU) and ``engine="polars-gpu"`` (GPU). +Every cell is the median of 5 timed runs after 2 warmups, and every task +returns the same result size on all three systems. One cell, LiveJournal GPU +PageRank, is the median of 3 runs after 1 warmup. See +:ref:`graphframes-method` for the full measurement rules. LiveJournal (35M edges) ----------------------- diff --git a/docs/source/gfql/index.rst b/docs/source/gfql/index.rst index f8711666ae..b9476a57bb 100644 --- a/docs/source/gfql/index.rst +++ b/docs/source/gfql/index.rst @@ -15,7 +15,8 @@ compile to vectorized dataframe operations, so one machine handles graphs of Where to start -------------- -- **New to GFQL**: :doc:`about` (10 minutes), then :doc:`quick`. +- **New to GFQL**: :doc:`about` (10 minutes), then :doc:`quick`, then the + :doc:`case study `. - **Coming from Cypher**: :doc:`cypher`, then :doc:`spec/cypher_mapping`. - **Need speed**: :doc:`engines` picks the engine. ``engine='polars'`` is the one-keyword CPU speedup; :doc:`performance` covers GPU and remote GPU. @@ -30,6 +31,7 @@ Where to start overview quick cypher + Case Study: Cypher + PageRank vs Neo4j .. toctree:: :maxdepth: 1 diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index 9dd893260e..ca043b90ed 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -9,15 +9,22 @@ Overview of GFQL New to GFQL, the open source dataframe-native graph query language? This article overviews the gaps it fills, special features like GPU accelerations, and where to go next. +GFQL is the only open-source Cypher implementation that runs in-process on +dataframes, with no database to install, load, or keep in sync. It brings the +columnar, vectorized execution model that DuckDB and ClickHouse made standard +for analytics to graph pattern matching. A query compiles to batched dataframe +operations over Arrow-backed columns on pandas, Polars, or NVIDIA cuDF. The +same query therefore runs on a laptop CPU or on a GPU. + Why GFQL? ~~~~~~~~~~~ -GFQL addresses a critical gap in the data community by providing an in-process graph query language that operates at the compute tier. This means you can: +GFQL is an in-process graph query language for the compute tier. With it you can: - **Graph search**: Easily and efficiently query and filter nodes and edges using a familiar syntax. - **Avoid External Infrastructure**: Avoid calls to external infrastructures and eliminate the need for extra databases. -- **Leverage Existing Workflows**: Integrate with your current Python data science tools and libraries. +- **Use Existing Workflows**: Integrate with your current Python data science tools and libraries. - **Achieve High Performance**: Utilize GPU acceleration for massive speedups in graph processing. - **Simplify Graph Analytics**: Write expressive and concise graph queries in Python. @@ -27,14 +34,12 @@ Key Features - **Dataframe-Native Integration**: Works directly with Pandas, Polars, cuDF, and Apache Arrow dataframes. - **High Performance**: Optimized for both CPU and GPU execution, capable of processing billions of edges. - **Ease of Use**: Install via `pip` and start querying without the need for external databases. -- **Seamless Visualization**: Integrated with PyGraphistry for GPU-accelerated graph visualization. +- **Visualization**: Integrated with PyGraphistry for GPU-accelerated graph visualization. - **Flexibility**: Suitable for a wide range of applications, including cybersecurity, fraud detection, financial analysis, and more. - **Architectural Freedom**: Use GFQL with your dataframes on your local CPU/GPU, or offload to a remote GPU cluster. Installation Guide ~~~~~~~~~~~~~~~~~~~ -.. toctree:: - :hidden: GFQL is built into pygraphistry: diff --git a/docs/source/gfql/perf/index.rst b/docs/source/gfql/perf/index.rst index b3ce3e760a..4389d5d239 100644 --- a/docs/source/gfql/perf/index.rst +++ b/docs/source/gfql/perf/index.rst @@ -9,8 +9,10 @@ engine comparisons. :doc:`../indexing` and :doc:`../index_adjacency` speed up queries that start from known nodes. :doc:`../remote` runs the same queries on a Graphistry server GPU. -The two benchmark pages compare GFQL with Neo4j + GDS and with Apache Spark -GraphFrames on one machine. +The :doc:`../benchmark_graphframes` page compares GFQL with Apache Spark +GraphFrames on one machine. The Start Here +:doc:`case study <../benchmark_filter_pagerank>` compares one Cypher + PageRank +pipeline with Neo4j + GDS. .. toctree:: :maxdepth: 1 @@ -20,5 +22,4 @@ GraphFrames on one machine. Pay-As-You-Go Resident Indexing <../indexing> Seeded Traversal Indexes <../index_adjacency> ../remote - Benchmark: Filter + PageRank vs Neo4j + GDS <../benchmark_filter_pagerank> Benchmark: vs Spark GraphFrames <../benchmark_graphframes> diff --git a/docs/test_bench_numbers.py b/docs/test_bench_numbers.py index 826f43178c..e2e65b7f30 100644 --- a/docs/test_bench_numbers.py +++ b/docs/test_bench_numbers.py @@ -309,7 +309,7 @@ def test_every_chart_matches_the_published_numbers(): """ stale = [] for name, svg in charts.rendered().items(): - path = os.path.join(charts.CHART_DIR, name) + path = charts.chart_path(name) if not os.path.exists(path): stale.append('{} is missing'.format(name)) continue From 2ad9453fe7276fbc77192c8896c5deeb19c8534d Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 17:07:15 -0700 Subject: [PATCH 03/29] docs(gfql): correct GraphFrames cell count (best engine 8/8, CPU alone 6/8); wrap chart strings Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 2 +- docs/source/_ext/gfql_bench_charts.py | 6 ++++-- docs/source/gfql/benchmark_graphframes.rst | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9202863519..49a89635f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Docs - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. -- **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome, keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). +- **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome (GraphFrames: GFQL's best engine faster in all eight cells, CPU alone in six), keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). - **GFQL overview states the positioning up front**: `gfql/overview` says GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone. ## [0.59.0 - 2026-08-31] diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index 8237df340a..edb63a26a2 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -277,7 +277,8 @@ def render_graphframes(name: str, results: JSONObject) -> str: edges = dataset.get('n_edges') nodes_ = dataset.get('n_nodes') title = '{}: {:,} nodes / {:,} edges'.format(label, int(nodes_), int(edges)) - subtitle = 'Median of 5 after 2 warmups; same result size on every system. Lower is better.' + subtitle = ('Median of 5 after 2 warmups; same result size on every system. ' + 'Lower is better.') group_h = GF_GROUP_HEAD + GF_BAR_ROW * len(GF_SYSTEMS) + GF_GROUP_GAP height = HEADER_H + group_h * len(GF_TASKS) + FOOT_H out = [ @@ -316,7 +317,8 @@ def render_graphframes(name: str, results: JSONObject) -> str: _num(bar_x + width + 8), row_top + 18, ''.join(spans))) out.append('{}'.format( PAD, height - 11, - _esc('Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores.'))) + _esc('Bars are scaled per task. dgx-spark, single node; ' + 'Spark local[*] over all cores.'))) out.append('') return '\n'.join(out) + '\n' diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index f59c66f777..4148635768 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -11,12 +11,13 @@ GFQL is Graphistry's open-source graph query language: Cypher and Python chains that run in-process on dataframes, with no database or cluster. GraphFrames is Spark's graph library, run here on ``local[*]`` (a single-node JVM using all cores). The workload is four tasks on two graphs, LiveJournal -(35M edges) and Orkut (117M edges). GFQL is faster than GraphFrames in seven of -the eight cells with the CPU engine alone. Graph filters and k-hop traversals -run 1.3x to 43x faster. The exception is whole-graph PageRank on CPU, where -GraphFrames beats GFQL's igraph path. On that task the GFQL GPU engine is 10x -to 15x faster than GraphFrames. Use GFQL on CPU for filters and traversals, -and on GPU for PageRank. +(35M edges) and Orkut (117M edges). GFQL's best engine is faster than +GraphFrames in all eight cells. The CPU engine alone is faster in six of the +eight: graph filters and k-hop traversals run 1.3x to 43x faster. The two +exceptions are whole-graph PageRank on CPU, where GraphFrames beats GFQL's +igraph path. On that task the GFQL GPU engine is 10x to 15x faster than +GraphFrames. Use GFQL on CPU for filters and traversals, and on GPU for +PageRank. .. image:: _static/graphframes/livejournal_tasks.svg :alt: LiveJournal task times: GFQL CPU and GPU versus GraphFrames for filter, 1-hop, 2-hop, and PageRank From 5fbf9cba9bc89e89b42317d6fafb7aca06843b51 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 22:15:50 -0700 Subject: [PATCH 04/29] docs(gfql): wave 3 wording pass: Start Here nav, plain index titles, engines table, tested Cypher twins - index: Start Here = about, overview, speedup case study; quick + cypher head the Language Reference hub (spec linked) - benchmark_filter_pagerank: "Speedup Case Study" title - indexing / index_adjacency: plain-language titles and cross-refs - overview: one "Why GFQL?" list (was Why + Key Features), engine-design and launch blog links - engines: opening note boxes folded into prose; "coming from" table with concrete change + measurement pointer; Memgraph row; PuppyGraph removed; section "Parity and fallback rules" - about: examples 3-7 gain tested Cypher twins; example 4 pattern now returns rows on the sample graph; sample-graph block is executable in the doc lane - slop words removed across gfql pages (leverage/seamless/honest/powerful/ critical gap); no numbers added Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 1 + docs/source/gfql/about.rst | 99 +++++++++++------ .../source/gfql/benchmark_filter_pagerank.rst | 4 +- docs/source/gfql/cypher.rst | 4 +- docs/source/gfql/engines.rst | 105 ++++++++---------- docs/source/gfql/index.rst | 13 +-- docs/source/gfql/index_adjacency.rst | 8 +- docs/source/gfql/indexing.rst | 10 +- docs/source/gfql/overview.rst | 36 +++--- docs/source/gfql/perf/index.rst | 6 +- docs/source/gfql/quick.rst | 2 +- docs/source/gfql/reference/index.rst | 12 +- docs/source/gfql/translate.rst | 10 +- docs/source/gfql/validation/fundamentals.rst | 2 +- docs/source/gfql/validation/llm.rst | 2 +- docs/source/gfql/wire_protocol_examples.md | 2 +- 16 files changed, 169 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a89635f5..01a554aa76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. - **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome (GraphFrames: GFQL's best engine faster in all eight cells, CPU alone in six), keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). +- **GFQL docs wording and structure pass**: Start Here is now the 10-minute guide, the overview, and the speedup case study; the quick reference and Cypher syntax guide head the Language Reference hub. `gfql/indexing` and `gfql/index_adjacency` carry plain-language titles. The engines page folds its opening note boxes into prose and its "coming from" table names the concrete change and where the measurement lives (Memgraph added, PuppyGraph dropped). `gfql/about` examples 3–7 gain tested Cypher twins, example 4 now returns rows on its own sample graph, and the sample-graph block runs in the doc-example lane so later examples are checked against it. Marketing filler ("leverage", "seamless", "honest") is removed across the GFQL pages. - **GFQL overview states the positioning up front**: `gfql/overview` says GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone. ## [0.59.0 - 2026-08-31] diff --git a/docs/source/gfql/about.rst b/docs/source/gfql/about.rst index 2eb79623ee..3b5cd0d646 100644 --- a/docs/source/gfql/about.rst +++ b/docs/source/gfql/about.rst @@ -8,22 +8,20 @@ :width: 200px :align: center -Welcome to **GFQL (GraphFrame Query Language)**, the first **dataframe-native graph query language**. GFQL is designed to bring the power of graph queries to your data science workflows without the need for external graph databases or complex infrastructure. It integrates seamlessly with the **PyData**, **Apache Arrow**, and **GPU acceleration** ecosystems, allowing you to process massive graphs efficiently. +Welcome to **GFQL (GraphFrame Query Language)**, the first **dataframe-native graph query language**. GFQL brings graph queries to data science workflows without an external graph database. It works with the **PyData**, **Apache Arrow**, and **GPU** ecosystems, so large graphs run in-process on CPU or GPU. In this guide, we'll explore the basics of GFQL in just 10 minutes. You'll learn how to: - Query and filter nodes and edges. - Chain multiple hops and apply predicates. -- Leverage automatic GPU acceleration. +- Use automatic GPU acceleration. - Integrate GFQL into your existing Python workflows. - Run GFQL and Python on remote GPUs and remote data. -Let's dive in! - Introduction to GFQL -------------------- -GFQL fills a critical gap in the data community by providing an in-process, high-performance graph query language that operates at the compute tier. Unlike traditional graph databases that couple storage and compute, GFQL allows you to perform graph queries directly on your dataframes, whether they're in-memory or on disk, CPU or GPU. +GFQL is an in-process graph query language for the compute tier. Graph databases couple storage and compute; GFQL queries the dataframes you already have, in memory, on CPU or GPU. **Key Benefits:** @@ -44,7 +42,7 @@ Throughout this guide, we'll work with a graph representing people, companies, a
Sample graph rendered with plot_static().
-:: +.. code-block:: python import pandas as pd import graphistry @@ -195,7 +193,7 @@ Label hops in your traversal to analyze specific relationships. **Example: Find nodes up to 2 hops away from node "a" and label each hop** -:: +.. code-block:: python from graphistry import n, e_undirected @@ -207,6 +205,14 @@ Label hops in your traversal to analyze specific relationships. first_hop_edges = g_2_hops._edges[ g_2_hops._edges.hop1 == True ] # first_hop_edges: edges directly connected to 'a' (hop1=True) +The Cypher form returns the same subgraph. Cypher has no per-hop label, so use +the chain form when you need the ``hop1`` / ``hop2`` columns: + +.. code-block:: python + + g_2_hops_cypher = g.gfql("GRAPH { MATCH (a {id: 'a'})-[hop1]-(b)-[hop2]-(c) }") + assert set(g_2_hops_cypher._nodes['id']) == set(g_2_hops._nodes['id']) + **Explanation:** - ``n({g._node: "a"})`` starts the traversal from node ``"a"`` where ``g._node`` is the identifying column name. @@ -238,28 +244,36 @@ Label hops in your traversal to analyze specific relationships. Chain multiple traversals to find patterns between nodes. -**Example: Find transaction nodes between two types of risky nodes** +**Example: Find risk2 transactions reachable from risk1 nodes** -:: +.. code-block:: python - from graphistry import n, e_forward, e_reverse + from graphistry import n, e_forward g_risky = g.gfql([ n({"risk1": True}), e_forward(to_fixed_point=True), - n({"type": "transaction"}, name="hit"), - e_reverse(to_fixed_point=True), - n({"risk2": True}) + n({"type": "transaction", "risk2": True}, name="hit") ]) hits = g_risky._nodes[ g_risky._nodes["hit"] == True ] - # hits: transaction nodes reachable from risk1 nodes and reaching risk2 nodes + assert sorted(hits['id']) == ['tx2'] + # hits: transaction nodes flagged risk2 that a risk1 node reaches (tx1 -> tx2) + +The Cypher form uses a variable-length path for ``to_fixed_point``: + +.. code-block:: python + + hits_df = g.gfql( + "MATCH (r {risk1: true})-[*1..]->(t {type: 'transaction', risk2: true}) " + "RETURN t.id AS id" + )._nodes + assert sorted(hits_df['id']) == ['tx2'] **Explanation:** -- Starts from nodes with ``risk1 == True``. -- Traverses forward to transaction nodes, labeling them as ``hit``. -- Traverses backward to nodes with ``risk2 == True``. -- Identifies transaction nodes connected between two risky nodes. +- Starts from nodes with ``risk1 == True`` (``tx1``). +- Follows forward edges to a fixed point (every node reachable from the start set). +- Keeps transaction nodes with ``risk2 == True`` and labels them ``hit`` (``tx2``). .. graphviz:: @@ -287,26 +301,37 @@ Use the ``is_in`` predicate to filter nodes or edges by multiple values. **Example: Filter nodes and edges by multiple types** -:: +.. code-block:: python - from graphistry import n, e_forward, e_reverse, is_in + from graphistry import n, e_forward, is_in g_filtered = g.gfql([ n({"type": is_in(["person", "company"])}), - e_forward({"e_type": is_in(["owns", "reviews"])}, to_fixed_point=True), - n({"type": is_in(["transaction", "account"])}, name="hit"), - e_reverse(to_fixed_point=True), - n({"risk2": True}) + e_forward({"e_type": is_in(["sent", "transfer"])}, to_fixed_point=True), + n({"type": is_in(["transaction", "account"])}, name="hit") ]) hits = g_filtered._nodes[ g_filtered._nodes["hit"] == True ] - # hits: transaction/account nodes matching the traversal pattern + assert sorted(hits['id']) == ['tx1', 'tx2'] + # hits: transaction/account nodes reached over sent/transfer edges (a -> tx1 -> tx2) + +In Cypher, ``is_in`` is ``IN``. This single-hop form returns the first transaction; +variable-length paths with ``IN`` filters on several aliases are not yet supported +(`#2019 `_): + +.. code-block:: python + + hits_df = g.gfql( + "MATCH (a)-[e]->(t {type: 'transaction'}) " + "WHERE a.type IN ['person', 'company'] AND e.e_type IN ['sent', 'transfer'] " + "RETURN t.id AS id" + )._nodes + assert sorted(hits_df['id']) == ['tx1'] **Explanation:** -- Filters nodes of type ``"person"`` or ``"company"``. -- Traverses forward edges of type ``"owns"`` or ``"reviews"``. -- Filters nodes of type ``"transaction"`` or ``"account"``, labeling them as ``hit``. -- Traverses backward to nodes with ``risk2 == True``. +- Filters start nodes of type ``"person"`` or ``"company"``. +- Follows forward edges of type ``"sent"`` or ``"transfer"`` to a fixed point. +- Keeps nodes of type ``"transaction"`` or ``"account"`` and labels them ``hit``. .. graphviz:: @@ -341,7 +366,7 @@ Use the ``is_in`` predicate to filter nodes or edges by multiple values. tx -> risk2 [label="e_reverse\n*", color="#DC143C", penwidth=2, style=bold, dir=back]; } -Leveraging GPU Acceleration +Using GPU Acceleration --------------------------- GFQL is optimized for GPU acceleration using ``cudf`` and ``rapids``. When using GPU dataframes, GFQL automatically executes queries on the GPU for massive speedups. @@ -363,8 +388,9 @@ GFQL is optimized for GPU acceleration using ``cudf`` and ``rapids``. When using # Create a graph with GPU dataframes g_gpu = graphistry.edges(e_gdf, 'src', 'dst').nodes(n_gdf, 'id') - # Run GFQL query (executes on GPU) + # Run GFQL query (executes on GPU); Cypher strings work the same way g_result = g_gpu.gfql([ ... ]) + g_result = g_gpu.gfql("MATCH (n {type: 'person'}) RETURN n") **Explanation:** @@ -380,9 +406,10 @@ results on every engine — see :doc:`Choosing an Engine `. **Example: CPU columnar speedup (no GPU)** -:: +.. code-block:: python - g_result = g.gfql([ ... ], engine='polars') # often much faster on query-heavy workloads + people = g.gfql("MATCH (n {type: 'person'}) RETURN n", engine='polars')._nodes + assert len(people) == 2 # same answer as the pandas engine **Example: Force GFQL to use a GPU engine** @@ -400,7 +427,7 @@ results on every engine — see :doc:`Choosing an Engine `. Integration with PyData Ecosystem --------------------------------- -GFQL integrates seamlessly with the PyData ecosystem, allowing you to combine it with libraries like ``pandas``, ``networkx``, ``igraph``, and ``PyTorch``. +GFQL works with the PyData ecosystem, so you can combine it with libraries like ``pandas``, ``networkx``, ``igraph``, and ``PyTorch``. 8. Combining GFQL with Graph Algorithms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -604,7 +631,7 @@ Congratulations! You've covered the basics of GFQL in just 10 minutes. You've le - Query and filter nodes and edges using GFQL. - Chain multiple hops and apply advanced predicates. -- Leverage GPU acceleration for high-performance graph querying. +- Use GPU acceleration for large graphs. - Integrate GFQL with graph algorithms and visualization tools. **Next Steps:** @@ -616,6 +643,6 @@ Congratulations! You've covered the basics of GFQL in just 10 minutes. You've le - :ref:`10min-pygraphistry`: Utilize PyGraphistry for advanced visualization and analysis. - :ref:`Join the Community `: Connect with other users and developers in the GFQL community Slack channel. -GFQL opens up new possibilities for graph analysis at scale, without the overhead of managing external databases or infrastructure. With its seamless integration into the Python ecosystem and support for GPU acceleration, GFQL is a powerful tool for modern data science workflows. +GFQL runs graph analysis at scale without a database to manage. It fits the Python ecosystem and moves to a GPU with one keyword. Happy graph querying! diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index 1960632181..cca3574461 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -1,5 +1,5 @@ -Case Study: Cypher + PageRank Pipeline, GFQL vs Neo4j + GDS -============================================================ +Speedup Case Study: Cypher + PageRank, GFQL vs Neo4j + GDS +=========================================================== .. image:: _static/gfql-mascot.png :alt: GFQL mascot diff --git a/docs/source/gfql/cypher.rst b/docs/source/gfql/cypher.rst index 546c2062c9..70101b80f8 100644 --- a/docs/source/gfql/cypher.rst +++ b/docs/source/gfql/cypher.rst @@ -275,7 +275,7 @@ WHERE Forms uppercase escape classes (e.g. ``(?i)\\D+``; lowercase ``\\d``/``\\.`` work), case-crossing character ranges (``(?i)[A-z]``), hex escapes, and non-ASCII patterns raise ``NotImplementedError`` (libcudf - regex limits — declined honestly rather than approximated); use + regex limits — declined rather than approximated); use ``engine='pandas'`` for those patterns. (``LIKE`` / ``ILIKE`` are not part of Cypher or GQL — use ``=~``, ``CONTAINS``, or ``STARTS WITH`` instead.) - Label predicates such as ``WHERE b:Foo:Bar``. @@ -340,7 +340,7 @@ and ``RETURN`` expressions: keys error, listing the valid ones). Composes with other WHERE predicates through AND/OR/NOT; nodes and edges independently searchable with different terms. Runs natively on all four engines for node aliases; an edge-alias - ``searchAny(r, ...)`` declines honestly on polars pending multi-entity + ``searchAny(r, ...)`` declines on polars pending multi-entity binding-row support (use ``engine='pandas'``), and explicit non-string columns beyond ints/bools likewise decline on polars and cuDF rather than risk divergent stringification (float repr differs across engines). The regex path obeys the same diff --git a/docs/source/gfql/engines.rst b/docs/source/gfql/engines.rst index 71c599a897..074723fe36 100644 --- a/docs/source/gfql/engines.rst +++ b/docs/source/gfql/engines.rst @@ -12,9 +12,8 @@ known statically, so the safety contract is same answer or pre-execution error, not silent fallback. Pick the engine that fits your hardware and workload; nothing else changes. -.. note:: - **New to GFQL?** This page assumes you already have a graph ``g`` and a ``query``. If not, - build one first — see :doc:`about` (10 Minutes to GFQL). +This page assumes you already have a graph ``g`` and a ``query``; if not, start with +:doc:`about`. The one-line speedup -------------------- @@ -44,26 +43,23 @@ than silently bridge), and the GPU engines only pay off on larger work. On CPU, Polars wins the common graph-query shapes (traversal, ``WHERE``/``ORDER``, aggregation) — see *When not to use Polars* below. -.. note:: - **Already a Polars user? The default now keeps you native.** With the default - ``engine='auto'``, a graph whose bound frames are all ``polars.DataFrame`` runs on the - Polars engine and returns Polars frames. If the query uses a shape the Polars engine - declines, GFQL falls back to pandas for that call — so ``auto`` is native *when it can - be*, and pandas otherwise. Pass ``engine='polars'`` explicitly when you want a decline - to raise instead of silently falling back: +**Already a Polars user?** With the default ``engine='auto'``, a graph whose bound frames +are all ``polars.DataFrame`` runs on the Polars engine and returns Polars frames. If the +query uses a shape the Polars engine declines, ``auto`` falls back to pandas for that call. +Pass ``engine='polars'`` when a decline should raise instead: - .. code-block:: python +.. doc-test: skip - import polars as pl, graphistry - g = graphistry.edges(edges_pl, 'src', 'dst').nodes(nodes_pl, 'id') # polars frames - out = g.gfql(query) # auto -> native Polars (out._nodes is polars) - out = g.gfql(query, engine='polars') # same, but a declined shape raises +.. code-block:: python -.. note:: - **Result frames match the engine.** With ``engine='polars'`` or ``'polars-gpu'`` the - output is Polars — ``result._nodes`` and ``result._edges`` are ``polars.DataFrame`` (and - ``cudf.DataFrame`` for ``engine='cudf'``). If downstream code is pandas-specific (``.iloc``, - ``.loc``, ``groupby().apply()``), call ``result._nodes.to_pandas()`` to convert back. + import polars as pl, graphistry + g = graphistry.edges(edges_pl, 'src', 'dst').nodes(nodes_pl, 'id') # polars frames + out = g.gfql(query) # auto -> native Polars (out._nodes is polars) + out = g.gfql(query, engine='polars') # same, but a declined shape raises + +**Result frames match the engine.** With ``engine='polars'`` or ``'polars-gpu'`` the output +frames are Polars, and ``cudf.DataFrame`` for ``engine='cudf'``. Pandas-only downstream code +(``.iloc``, ``groupby().apply()``) gets a pandas frame with ``result._nodes.to_pandas()``. The four engines ---------------- @@ -137,50 +133,45 @@ you already have, in your own process. There is no server to stand up, no ETL to projection step, no cluster to size. The query, the analytic, and the scoring stay in one pipeline over one set of frames. -The table says what you get when the query runs there instead of somewhere else. The -measured comparison, with its lane and its provenance, is on the :doc:`performance` page. +The table names the concrete change for each system and where the measured comparison +lives. Every figure on those pages renders from a committed pyg-bench artifact. .. list-table:: :header-rows: 1 - :widths: 18 40 42 + :widths: 16 30 54 * - Coming from - - Written in Cypher today? - - What GFQL gives you + - What changes + - What you gain, and where it is measured * - **Neo4j + GDS** - - Yes — GFQL accepts the same ``MATCH ... RETURN`` shapes. - - Filter → PageRank → filter runs as one in-process call: no graph projection, no - write-back, no round trip. The pipeline and its reproducer are in - :doc:`benchmark_filter_pagerank`. + - Same ``MATCH ... RETURN`` Cypher; no server, no GDS projection, no write-back. + - One in-process call runs filter, PageRank, and filter over resident frames, on CPU + or GPU. The measured pipeline times against Neo4j + GDS on the 30M-edge GPlus graph + are in :doc:`benchmark_filter_pagerank`. + * - **Memgraph** + - Same Cypher; no server round trip. + - Point lookups are Memgraph's strength: on the SNB-derived point queries the graph + databases, Memgraph first, beat GFQL. GFQL's wins are bulk shapes: traversals from + seed sets and global aggregates. See :doc:`performance`. * - **Kuzu** - - Yes. - - The measured q1–q9 board is against embedded Kuzu; see - :ref:`gfql-vs-kuzu-board` for the lane, the per-query numbers, and the losses. The - GFQL side queries a frame that is already in memory — nothing to load, nothing to - index first. + - Same Cypher; query the frame already in memory, nothing to load or index first. + - The q1–q9 board on :doc:`performance` is the measured comparison, per query, with + the losses shown. * - **LadybugDB** - - Yes. - - The same dataframe-native path: in-process, GPU-capable, no separate store. Polars - streaming (``GFQL_POLARS_CPU_STREAMING=1``) and the cudf-polars streaming executor - (``GFQL_POLARS_GPU_EXECUTOR=streaming``) spill query intermediates and results - beyond RAM. + - Same dataframe-native path, in process. + - Polars streaming (``GFQL_POLARS_CPU_STREAMING=1``) and the cudf-polars streaming + executor (``GFQL_POLARS_GPU_EXECUTOR=streaming``) spill intermediates and results + beyond RAM. Scan-shaped queries are measured on :doc:`performance`. * - **networkx** - - No — GFQL adds a declarative query language over the same graph. - - Columnar CPU execution and a one-keyword move to GPU, on frames rather than Python - objects. + - A declarative query language over the graph, on frames instead of Python objects. + - Columnar CPU execution and a one-keyword move to GPU. * - **igraph** - - No. - - igraph is the CPU PageRank backend *inside* GFQL, so you keep it and gain the query - layer, the Polars engines, and the GPU path. + - Nothing to give up: igraph is GFQL's CPU PageRank backend. + - The query layer, the Polars engines, and the GPU path on top of igraph analytics. * - **Spark GraphFrames** - - No — GFQL is Cypher; GraphFrames is a DataFrame API. - - Single-node execution with interactive latency and no cluster to provision or tune. - The measured head-to-head, with its committed raw results, is in - :doc:`benchmark_graphframes`. - * - **PuppyGraph** - - Yes. - - GPU and CPU graph **analytics** — PageRank, centrality, community — on the pulled - subgraph, in the same pipeline as the query. + - Cypher instead of a DataFrame API; single node, no cluster. + - Interactive latency for filters and traversals on CPU, and GPU PageRank. The + head-to-head, with committed results, is :doc:`benchmark_graphframes`. Route by shape: **selective** seeded lookups favor the GFQL resident index, **scan and aggregate** volume favors Polars, and **bulk** frontier expansion and full pipelines favor @@ -253,7 +244,7 @@ row count from one join) raw ``cudf`` leads and ``polars-gpu`` slips as its in-m GPU executor comes under memory pressure. Prefer ``cudf`` for that regime. **[F4] Polars-GPU is GPU-or-error.** It never silently falls back to CPU and reports the -result as a GPU run (see *Honesty* below). +result as a GPU run (see *Parity and fallback rules* below). **[F5] Selective traversal is an indexing problem, not an engine choice.** A seeded ``hop`` from a few nodes is fastest with the opt-in **CSR adjacency index** (``g.gfql_index_all()`` / @@ -476,12 +467,12 @@ Three cases, stated so you can route around them: ``WHERE``, some temporal/entity-text forms). GFQL rejects those shapes during validation, compilation, or planning before query execution and points at ``engine='pandas'`` — it **never** silently bridges Polars to pandas, because that would - misreport pandas performance as Polars (see *Honesty*). + misreport pandas performance as Polars (see *Parity and fallback rules*). - **One extreme materialization (a huge output row count):** prefer ``cudf`` over ``polars-gpu`` (footnote F3). -Parity and honesty ------------------- +Parity and fallback rules +------------------------- - **Identical results across engines.** Differential parity — every engine's output must match the pandas oracle — is a release gate, exercised across forward/reverse/undirected, 1-3 hop, diff --git a/docs/source/gfql/index.rst b/docs/source/gfql/index.rst index b9476a57bb..aef1b1e817 100644 --- a/docs/source/gfql/index.rst +++ b/docs/source/gfql/index.rst @@ -15,13 +15,14 @@ compile to vectorized dataframe operations, so one machine handles graphs of Where to start -------------- -- **New to GFQL**: :doc:`about` (10 minutes), then :doc:`quick`, then the - :doc:`case study `. -- **Coming from Cypher**: :doc:`cypher`, then :doc:`spec/cypher_mapping`. +- **New to GFQL**: :doc:`about` (10 minutes), :doc:`overview`, then the + :doc:`speedup case study `. +- **Coming from Cypher**: :doc:`cypher` and :doc:`quick`, then + :doc:`spec/cypher_mapping`. - **Need speed**: :doc:`engines` picks the engine. ``engine='polars'`` is the one-keyword CPU speedup; :doc:`performance` covers GPU and remote GPU. - **Start from known nodes**: :doc:`indexing` and :doc:`index_adjacency` make - seeded lookups cost O(degree) instead of O(E). + lookups from known nodes cost O(degree) instead of O(E). .. toctree:: :maxdepth: 1 @@ -29,9 +30,7 @@ Where to start about overview - quick - cypher - Case Study: Cypher + PageRank vs Neo4j + Speedup Case Study: vs Neo4j .. toctree:: :maxdepth: 1 diff --git a/docs/source/gfql/index_adjacency.rst b/docs/source/gfql/index_adjacency.rst index 3574e575a4..2d7dde9a09 100644 --- a/docs/source/gfql/index_adjacency.rst +++ b/docs/source/gfql/index_adjacency.rst @@ -1,5 +1,5 @@ -Seeded Traversal Indexes (CSR Adjacency) -======================================== +Adjacency Index: Fast Lookups from Known Nodes +============================================== A **seeded** graph query starts from a known set of nodes — "the neighbors of these 50 accounts", "2 hops out from this device" — rather than scanning the whole graph. @@ -123,8 +123,8 @@ nothing it cannot trace to a committed artifact. Reproducers: ``benchmarks/gfql/index_takeover_bench.py``, ``benchmarks/gfql/index_vs_dbs.py``, ``benchmarks/gfql/index_vs_kuzu_prepared.py``. -Honesty and cost ----------------- +Cost and fallback +----------------- - **Build cost** is one ``O(E log E)`` sort, amortized over subsequent queries. ``index_policy='auto'`` only builds when the planner predicts a selective query will diff --git a/docs/source/gfql/indexing.rst b/docs/source/gfql/indexing.rst index 39c18fbc8a..59895fe5b6 100644 --- a/docs/source/gfql/indexing.rst +++ b/docs/source/gfql/indexing.rst @@ -1,7 +1,7 @@ .. _gfql-indexing: -Pay-As-You-Go Resident Indexing -=============================== +Indexing Guide: Build Once, Query Faster +======================================== GFQL runs without any indexes: every query is a vectorized scan over your dataframes. When your workload is **seeded** — "expand from these 50 accounts", "look up this id and @@ -9,7 +9,7 @@ hop out" — you can opt into **resident indexes**: build them once with one cal seeded queries reuse them automatically after that. This page is the user guide to that lifecycle: what the indexes are, what engages them, when they go stale, and what they cost. For the planner policy knobs and competitive benchmarks, see -:doc:`Seeded Traversal Indexes `. +:doc:`Adjacency Index `. .. doc-test: skip @@ -127,7 +127,7 @@ When several indexed columns appear in one seed predicate, the planner gathers o **most selective** one (estimated for free from the index's own offsets) and applies the remaining predicates to those candidates, so results never depend on which index happens to be resident. As with every kind, a missing, stale, or cost-gated-out index -simply falls back to the scan. +falls back to the scan. What uses the index today ------------------------- @@ -224,7 +224,7 @@ only when they trace to a committed benchmark artifact. See also -------- -- :doc:`Seeded Traversal Indexes ` — the planner (``index_policy``), +- :doc:`Adjacency Index ` — the planner (``index_policy``), Cypher DDL / wire protocol forms, and the index cost model. - :doc:`engines` — choosing pandas / Polars / cuDF / Polars-GPU. - :doc:`performance` — the vectorization + GPU design behind GFQL. diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index ca043b90ed..9188217021 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -22,21 +22,21 @@ Why GFQL? GFQL is an in-process graph query language for the compute tier. With it you can: -- **Graph search**: Easily and efficiently query and filter nodes and edges using a familiar syntax. -- **Avoid External Infrastructure**: Avoid calls to external infrastructures and eliminate the need for extra databases. -- **Use Existing Workflows**: Integrate with your current Python data science tools and libraries. -- **Achieve High Performance**: Utilize GPU acceleration for massive speedups in graph processing. -- **Simplify Graph Analytics**: Write expressive and concise graph queries in Python. - -Key Features -~~~~~~~~~~~~~ - -- **Dataframe-Native Integration**: Works directly with Pandas, Polars, cuDF, and Apache Arrow dataframes. -- **High Performance**: Optimized for both CPU and GPU execution, capable of processing billions of edges. -- **Ease of Use**: Install via `pip` and start querying without the need for external databases. -- **Visualization**: Integrated with PyGraphistry for GPU-accelerated graph visualization. -- **Flexibility**: Suitable for a wide range of applications, including cybersecurity, fraud detection, financial analysis, and more. -- **Architectural Freedom**: Use GFQL with your dataframes on your local CPU/GPU, or offload to a remote GPU cluster. +- **Query graphs where the data already is**: nodes and edges are pandas, Polars, cuDF, or + Apache Arrow dataframes. No database to install or load. +- **Write graph search in a familiar syntax**: Cypher strings or composable Python chains. +- **Run the same query on CPU or GPU**: one keyword switches between pandas, Polars, + cuDF, and Polars-GPU, with identical results. +- **Scale on one machine**: columnar, vectorized execution over graphs of 100M+ edges. +- **Stay in the Python workflow**: results are dataframes, so PyGraphistry visualization, + ML, and the rest of the PyData stack apply directly. +- **Move work to a remote GPU** when the data or the hardware lives elsewhere. + +Typical uses include cybersecurity, fraud detection, financial analysis, and knowledge +graphs. The engine design is described in the blog post +`Cypher on Polars: a CPU and GPU graph engine `_; +the original launch post is +`GFQL, OpenTelemetry, and more in PyGraphistry 2.40 `_. Installation Guide ~~~~~~~~~~~~~~~~~~~ @@ -56,14 +56,14 @@ Key GFQL Concepts GFQL works on the same graphs as the rest of the PyGraphistry library. The operations run on top of the dataframe engine of your choice, with initial support for Pandas dataframes (CPU) and cuDF dataframes (GPU). -- **Nodes and Edges**: Represented using dataframes, making integration with Pandas and cuDF seamless +- **Nodes and Edges**: Represented using dataframes, so they integrate directly with pandas and cuDF - **Cypher strings**: Write queries as Cypher strings — ``g.gfql("MATCH (n) WHERE n.score > 5 RETURN n")`` - **Native chains**: Or compose queries as Python objects — ``g.gfql([n({"score": gt(5)})])`` - **Predicates**: Apply conditions to filter nodes and edges based on their properties, reusing the optimized native operations of the underlying dataframe engine - **Same-path constraints (WHERE)**: Relate attributes across steps in a chain using `where` - **Row pipelines (`MATCH ... RETURN` style)**: Move from graph pattern matches to tabular results with `rows()`, `where_rows()`, `return_()`, `order_by()`, `group_by()`, `skip()`, and `limit()` - **Result kinds**: Some stages keep you in graph state, while row-pipeline stages and row-returning local Cypher `CALL` queries move you into row state -- **GPU & CPU vectorization**: GFQL automatically leverages GPU acceleration and in-memory columnar processing for massive speedups on your queries +- **GPU & CPU vectorization**: GFQL automatically uses GPU acceleration and in-memory columnar processing for massive speedups on your queries - **Optional remote mode**: Bind to remote data or upload it quickly as Arrow, and run your same Python and GFQL queries on remote GPU resources when available Choosing Entry Points And Result Kinds @@ -318,7 +318,7 @@ Key advantages of GFQL Let: - **GPU preservation**: All operations maintain GPU acceleration when available - **Clean semantics**: Express complex graph analyses as clear, declarative DAGs -Leveraging GPU Acceleration +Using GPU Acceleration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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'`` follows the input frames — **a Polars-frame graph runs natively on Polars under the default** — resolving to ``cudf`` for cuDF input and ``pandas`` otherwise; an all-cuDF graph is additionally tried on ``polars-gpu`` when that GPU path probes usable. A query shape the native engine declines falls back to ``pandas`` (or ``cudf``); pass the engine explicitly to get an error instead of a fallback. Neither engine silently bridges mid-query: ``polars-gpu`` is GPU-or-error, and unsupported Polars/Cypher shapes are declined during validation, compilation, or planning — before execution — so the fallback re-runs the query from the start on pandas rather than half-executing. See :doc:`Choosing an Engine ` for the decision matrix and benchmarks. diff --git a/docs/source/gfql/perf/index.rst b/docs/source/gfql/perf/index.rst index 4389d5d239..2566848abe 100644 --- a/docs/source/gfql/perf/index.rst +++ b/docs/source/gfql/perf/index.rst @@ -6,7 +6,7 @@ How to make GFQL fast, and what it measures against other systems. Start with :doc:`../engines` to pick pandas, Polars, cuDF, or Polars-GPU. :doc:`../performance` covers vectorization, GPU acceleration, and measured engine comparisons. :doc:`../indexing` and :doc:`../index_adjacency` speed up -queries that start from known nodes. :doc:`../remote` runs the same queries on +queries that start from known nodes (a watchlist, a session, a seed set). :doc:`../remote` runs the same queries on a Graphistry server GPU. The :doc:`../benchmark_graphframes` page compares GFQL with Apache Spark @@ -19,7 +19,7 @@ pipeline with Neo4j + GDS. Choosing an Engine <../engines> CPU and GPU Acceleration <../performance> - Pay-As-You-Go Resident Indexing <../indexing> - Seeded Traversal Indexes <../index_adjacency> + Indexing Guide <../indexing> + Adjacency Index for Lookups from Known Nodes <../index_adjacency> ../remote Benchmark: vs Spark GraphFrames <../benchmark_graphframes> diff --git a/docs/source/gfql/quick.rst b/docs/source/gfql/quick.rst index bc94fa9933..0b5f52e083 100644 --- a/docs/source/gfql/quick.rst +++ b/docs/source/gfql/quick.rst @@ -767,7 +767,7 @@ Tips and Best Practices - **Limit hops for performance:** Specify `hops` to control traversal depth. - **Use naming for analysis:** Apply `name` to label and filter results. - **Combine filters:** Use `filter_dict` and `query` for precise matching. -- **Leverage GPU acceleration:** Use `engine='cudf'` for large datasets. +- **Use GPU acceleration:** Use `engine='cudf'` for large datasets. - **Avoid infinite loops:** Be cautious with `to_fixed_point=True` in cyclic graphs. Examples at a Glance diff --git a/docs/source/gfql/reference/index.rst b/docs/source/gfql/reference/index.rst index e2c0ec21f8..46aa6f12f9 100644 --- a/docs/source/gfql/reference/index.rst +++ b/docs/source/gfql/reference/index.rst @@ -1,15 +1,19 @@ Language Reference ================== -Reference pages for the GFQL query language: filtering, projection, -predicates, built-in calls, policies, validation modes, and wire formats. +Reference pages for the GFQL query language: the quick reference and Cypher +syntax guide, then filtering, projection, predicates, built-in calls, +policies, validation modes, and wire formats. -For a guided introduction, see :doc:`../about`. For the formal specification, -see :doc:`../spec/index`. +For a guided introduction, see :doc:`../about`. For the formal language +specification, wire protocol, and embedding contracts, see +:doc:`../spec/index`. .. toctree:: :maxdepth: 1 + ../quick + ../cypher ../where ../return ../predicates/quick diff --git a/docs/source/gfql/translate.rst b/docs/source/gfql/translate.rst index ff69da64a0..c17f6b43d9 100644 --- a/docs/source/gfql/translate.rst +++ b/docs/source/gfql/translate.rst @@ -8,9 +8,9 @@ This guide provides a comparison between **SQL**, **Pandas**, **Cypher**, and ** Introduction ------------ -GFQL (GraphFrame Query Language) is designed to be intuitive for users familiar with SQL, Cypher, or dataframe like Pandas and Spark. By comparing equivalent queries across these languages, you can quickly grasp GFQL's syntax, benefits, and start utilizing its powerful graph querying capabilities within your workflows. +GFQL (GraphFrame Query Language) is designed to be intuitive for users familiar with SQL, Cypher, or dataframe like Pandas and Spark. By comparing equivalent queries across these languages, you can quickly grasp GFQL's syntax, benefits, and start utilizing its useful graph querying capabilities within your workflows. -GFQL operates on graph DataFrames - graphs represented as node and edge DataFrames. This DataFrame-native approach enables seamless integration with the PyData ecosystem and natural vectorization for both CPU and GPU processing. +GFQL operates on graph DataFrames - graphs represented as node and edge DataFrames. This DataFrame-native approach enables direct integration with the PyData ecosystem and natural vectorization for both CPU and GPU processing. GFQL accepts both **native chain syntax** (``g.gfql([n(), e(), n()])``) and **Cypher strings** (``g.gfql("MATCH ...")``). Most examples below show both @@ -608,7 +608,7 @@ Time-Windowed Graph Analytics - **SQL** and **Pandas**: These versions incorrectly simplify to a two-hop relationships; for multihop scenarios, refer to :ref:`all-paths`. -- **GFQL**: Utilizes the ``chain`` method to filter edges between ``"Alice"`` and ``"Bob"`` based on a timestamp within the last 7 days. This approach allows for multihop relationships as it leverages the graph's structure, and further using cuDF for GPU acceleration when available. +- **GFQL**: Utilizes the ``chain`` method to filter edges between ``"Alice"`` and ``"Bob"`` based on a timestamp within the last 7 days. This approach allows for multihop relationships as it uses the graph's structure, and further using cuDF for GPU acceleration when available. --- @@ -700,7 +700,7 @@ This example builds on the previous one, showing how **GFQL** handles parallel e - **In CPU environments**, the bulk processing model accelerates query execution algorithmically and takes advantage of hardware parallelism, improving efficiency. -- **In GPU mode**, GFQL **natively parallelizes** pathfinding, further leveraging hardware acceleration to process multiple paths concurrently and quickly, making it highly efficient for large-scale graph traversals. +- **In GPU mode**, GFQL **natively parallelizes** pathfinding, further using hardware acceleration to process multiple paths concurrently and quickly, making it highly efficient for large-scale graph traversals. --- @@ -782,6 +782,6 @@ Additional Resources Conclusion ---------- -GFQL bridges the gap between traditional querying languages and graph analytics. By translating queries from SQL, Pandas, and Cypher into GFQL, you can leverage powerful graph queries within your Python workflows. +GFQL bridges the gap between traditional querying languages and graph analytics. By translating queries from SQL, Pandas, and Cypher into GFQL, you can use useful graph queries within your Python workflows. Start exploring GFQL today and unlock new insights from your graph data! diff --git a/docs/source/gfql/validation/fundamentals.rst b/docs/source/gfql/validation/fundamentals.rst index 394a627e2f..f31fe1ceee 100644 --- a/docs/source/gfql/validation/fundamentals.rst +++ b/docs/source/gfql/validation/fundamentals.rst @@ -1,7 +1,7 @@ GFQL Validation Fundamentals ============================ -Learn how to use GFQL's built-in validation system to catch errors early and build robust graph applications. +Learn how to use GFQL's built-in validation system to catch errors early and build reliable graph applications. .. note:: This guide is accompanied by an interactive Jupyter notebook. To run the examples yourself, see diff --git a/docs/source/gfql/validation/llm.rst b/docs/source/gfql/validation/llm.rst index a6c63f71c4..de526aeb8e 100644 --- a/docs/source/gfql/validation/llm.rst +++ b/docs/source/gfql/validation/llm.rst @@ -192,7 +192,7 @@ Best Practices -------------- 1. **Built-in Validation**: Use GFQL's automatic validation during construction -2. **Error Codes**: Leverage structured error codes (E1xx, E2xx, E3xx) for programmatic handling +2. **Error Codes**: Use structured error codes (E1xx, E2xx, E3xx) for programmatic handling 3. **Collect-All Mode**: Use ``collect_all=True`` for comprehensive error reporting to LLMs 4. **Schema Context**: Provide available columns and types in LLM prompts 5. **Pre-execution Validation**: Validate schema before expensive operations diff --git a/docs/source/gfql/wire_protocol_examples.md b/docs/source/gfql/wire_protocol_examples.md index 01db9e8ddf..9b10bc2fda 100644 --- a/docs/source/gfql/wire_protocol_examples.md +++ b/docs/source/gfql/wire_protocol_examples.md @@ -575,7 +575,7 @@ filter3 = n(filter_dict={"date": gt({"type": "datetime", "value": "2023-01-01T00 ## Performance Considerations -- Temporal predicates leverage pandas' optimized datetime operations +- Temporal predicates use pandas' optimized datetime operations - Timezone conversions are handled efficiently - For large datasets, ensure datetime columns are properly typed (not object dtype) - Use `pd.Timestamp` for best performance when creating many predicates programmatically From 960f2a23b9b02623d2ea100278a595cd15486014 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 22:33:45 -0700 Subject: [PATCH 05/29] docs(gfql): performance page from the relaned four-engine board and SNB matrix; cell-derived tallies; drift enforcement - vendor pyg-bench published/docs-numbers.json @ main 7426456 (relane board with Kuzu/Memgraph/Neo4j, snb_aligned SF0.1/SF1, drift waivers) - _ext/gfql_bench_data: :bench-tally: (strict "N of M" from cells, registers refs) and max_compute_commit_drift enforcement via git rev-list on graphistry/compute, honoring policy.drift_waivers; None on shallow clones - performance.rst: no release-pinned headings; legacy untraceable literals removed; SNB tables with losses stated; five-engine q1-q9 boards; single Measurement block with caveats; q8 rendered as a result (cold binding) - overview: where GFQL wins and where databases win, with links - tests: tally, drift (fail / waived / unknown), vendored runs within policy Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 1 + docs/.rstcheck.cfg | 3 +- docs/source/_data/gfql_benchmarks.json | 1749 +++++++++++++++++++++--- docs/source/_ext/gfql_bench.py | 23 + docs/source/_ext/gfql_bench_data.py | 86 ++ docs/source/gfql/overview.rst | 7 + docs/source/gfql/performance.rst | 448 +++--- docs/test_bench_numbers.py | 76 + 8 files changed, 1948 insertions(+), 445 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a554aa76..99bc262fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. - **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome (GraphFrames: GFQL's best engine faster in all eight cells, CPU alone in six), keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). +- **GFQL performance page renders the relaned four-engine q1–q9 board and the aligned SNB matrix**: the vendored pyg-bench artifact now carries the 2026-08-13 board (GFQL bound cold per timed run) with Kuzu, Memgraph, and Neo4j columns and the SNB-derived SF0.1/SF1 four-engine tables, where the databases win every point-lookup cell. `gfql/performance` drops its release-pinned heading and every hand-typed legacy figure (typed-hop fast path, index timings, the old Neo4j five-query table, the bulk engine sweep, LadybugDB) in favor of published cells, one Measurement block, and win counts derived from cells by a new `:bench-tally:` role. The bench extension now enforces `policy.max_compute_commit_drift` against the checkout (git history permitting) with per-run waivers carried in the artifact, so a board measured long before the current compute code fails the build instead of aging silently. `gfql/overview` states where GFQL wins and where the databases win, with links. - **GFQL docs wording and structure pass**: Start Here is now the 10-minute guide, the overview, and the speedup case study; the quick reference and Cypher syntax guide head the Language Reference hub. `gfql/indexing` and `gfql/index_adjacency` carry plain-language titles. The engines page folds its opening note boxes into prose and its "coming from" table names the concrete change and where the measurement lives (Memgraph added, PuppyGraph dropped). `gfql/about` examples 3–7 gain tested Cypher twins, example 4 now returns rows on its own sample graph, and the sample-graph block runs in the doc-example lane so later examples are checked against it. Marketing filler ("leverage", "seamless", "honest") is removed across the GFQL pages. - **GFQL overview states the positioning up front**: `gfql/overview` says GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone. diff --git a/docs/.rstcheck.cfg b/docs/.rstcheck.cfg index 322035f4f4..ada8b4c969 100644 --- a/docs/.rstcheck.cfg +++ b/docs/.rstcheck.cfg @@ -22,7 +22,8 @@ ignore_roles = py:obj, py:data, bench, - bench-diag + bench-diag, + bench-tally # Ignore Sphinx-specific directives ignore_directives = diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 0d33a461e1..1cc4e53140 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -8,10 +8,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 153.16, + "value": 151.3, + "workload": "graph-benchmark 100k q1" + }, + "graphbench.100k.q1.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 655.5, + "workload": "graph-benchmark 100k q1" + }, + "graphbench.100k.q1.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 680.72, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.pandas": { @@ -21,11 +49,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 199.43, + "value": 173.86, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars": { @@ -35,13 +63,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 42.14, + "value": 26.49, "workload": "graph-benchmark 100k q1" }, + "graphbench.100k.q1.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q1.polars", + "graphbench.100k.q1.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 6.6, + "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q2.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -50,10 +95,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 279.17, + "workload": "graph-benchmark 100k q2" + }, + "graphbench.100k.q2.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 468.94, + "workload": "graph-benchmark 100k q2" + }, + "graphbench.100k.q2.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 281.91, + "value": 632.7, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.pandas": { @@ -63,11 +136,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 208.88, + "value": 183.72, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars": { @@ -77,13 +150,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 55.76, + "value": 38.84, "workload": "graph-benchmark 100k q2" }, + "graphbench.100k.q2.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q2.polars", + "graphbench.100k.q2.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 4.7, + "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q3.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -92,10 +182,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 34.16, + "workload": "graph-benchmark 100k q3" + }, + "graphbench.100k.q3.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 30.17, + "workload": "graph-benchmark 100k q3" + }, + "graphbench.100k.q3.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 28.76, + "value": 51.14, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.pandas": { @@ -105,11 +223,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 73.72, + "value": 76.29, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars": { @@ -119,13 +237,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 14.74, + "value": 9.9, "workload": "graph-benchmark 100k q3" }, + "graphbench.100k.q3.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q3.polars", + "graphbench.100k.q3.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 7.7, + "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q4.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -134,10 +269,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 13.78, + "workload": "graph-benchmark 100k q4" + }, + "graphbench.100k.q4.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 37.64, + "workload": "graph-benchmark 100k q4" + }, + "graphbench.100k.q4.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 11.56, + "value": 63.76, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.pandas": { @@ -147,11 +310,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 64.09, + "value": 69.28, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars": { @@ -161,13 +324,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 12.13, + "value": 10.15, "workload": "graph-benchmark 100k q4" }, + "graphbench.100k.q4.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q4.polars", + "graphbench.100k.q4.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 6.8, + "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q5.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -176,10 +356,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 12.54, + "workload": "graph-benchmark 100k q5" + }, + "graphbench.100k.q5.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 12.88, + "value": 3.95, + "workload": "graph-benchmark 100k q5" + }, + "graphbench.100k.q5.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 8.96, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.pandas": { @@ -189,11 +397,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 410.05, + "value": 411.6, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars": { @@ -203,13 +411,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 17.52, + "value": 11.33, "workload": "graph-benchmark 100k q5" }, + "graphbench.100k.q5.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q5.polars", + "graphbench.100k.q5.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 36.3, + "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q6.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -218,10 +443,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 24.42, + "workload": "graph-benchmark 100k q6" + }, + "graphbench.100k.q6.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 5.14, + "workload": "graph-benchmark 100k q6" + }, + "graphbench.100k.q6.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 20.73, + "value": 22.94, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.pandas": { @@ -231,11 +484,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 412.3, + "value": 410.3, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars": { @@ -245,13 +498,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 21.99, + "value": 12.24, "workload": "graph-benchmark 100k q6" }, + "graphbench.100k.q6.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q6.polars", + "graphbench.100k.q6.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 33.5, + "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q7.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -260,10 +530,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 9.57, + "workload": "graph-benchmark 100k q7" + }, + "graphbench.100k.q7.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 3.74, + "workload": "graph-benchmark 100k q7" + }, + "graphbench.100k.q7.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 8.85, + "value": 140.18, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.pandas": { @@ -273,11 +571,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 119.63, + "value": 120.63, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars": { @@ -287,13 +585,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 16.12, + "value": 6.94, "workload": "graph-benchmark 100k q7" }, + "graphbench.100k.q7.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q7.polars", + "graphbench.100k.q7.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 17.4, + "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q8.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -302,44 +617,85 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 13.45, + "workload": "graph-benchmark 100k q8" + }, + "graphbench.100k.q8.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 7316.46, + "workload": "graph-benchmark 100k q8" + }, + "graphbench.100k.q8.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 9.76, + "value": 3766.74, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.pandas": { - "board_quotable": false, - "comparison_allowed": false, + "board_quotable": true, + "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, - "disclosures": [ - "Measured with a harness that reused ONE Plottable across iterations, so pygraphistry's cross-call degree-count memo (pygraphistry #1825) made every timed run a cache HIT. One-shot, the same query is 8.89 ms at 20k and 34.11 ms at 100k on polars -- a LOSS to Kuzu, not a win. Re-measured in results/graphbench-q8-binding-20260729/; the memo hits ALL engines, so the pandas figure is flattered too." - ], + "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 91.6, + "value": 88.78, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars": { - "board_quotable": false, - "comparison_allowed": false, + "board_quotable": true, + "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, - "disclosures": [ - "Measured with a harness that reused ONE Plottable across iterations, so pygraphistry's cross-call degree-count memo (pygraphistry #1825) made every timed run a cache HIT. One-shot, the same query is 8.89 ms at 20k and 34.11 ms at 100k on polars -- a LOSS to Kuzu, not a win. Re-measured in results/graphbench-q8-binding-20260729/; the memo hits ALL engines, so the pandas figure is flattered too." - ], + "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 5.06, + "value": 13.6, "workload": "graph-benchmark 100k q8" }, + "graphbench.100k.q8.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q8.polars", + "graphbench.100k.q8.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 6.5, + "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.100k.q9.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -348,10 +704,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 84.32, + "workload": "graph-benchmark 100k q9" + }, + "graphbench.100k.q9.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "ms", + "value": 6199.8, + "workload": "graph-benchmark 100k q9" + }, + "graphbench.100k.q9.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 84.06, + "value": 4843.46, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.pandas": { @@ -361,11 +745,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 212.0, + "value": 162.06, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars": { @@ -375,13 +759,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260813", "status": "ok", "unit": "ms", - "value": 66.61, + "value": 36.33, "workload": "graph-benchmark 100k q9" }, + "graphbench.100k.q9.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q9.polars", + "graphbench.100k.q9.pandas" + ], + "run": "graphbench-q1q9-100k-20260813", + "status": "ok", + "unit": "x", + "value": 4.5, + "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q1.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -390,41 +791,86 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 14.51, + "value": 15.77, "workload": "graph-benchmark 20k q1" }, - "graphbench.20k.q1.pandas": { + "graphbench.20k.q1.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "memgraph", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 35.46, + "value": 66.08, "workload": "graph-benchmark 20k q1" }, - "graphbench.20k.q1.polars": { + "graphbench.20k.q1.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "neo4j", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 13.13, + "value": 74.5, "workload": "graph-benchmark 20k q1" }, - "graphbench.20k.q2.kuzu": { + "graphbench.20k.q1.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 27.6, + "workload": "graph-benchmark 20k q1" + }, + "graphbench.20k.q1.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 7.34, + "workload": "graph-benchmark 20k q1" + }, + "graphbench.20k.q1.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q1.polars", + "graphbench.20k.q1.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 3.8, + "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q2.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", @@ -432,10 +878,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 31.62, + "value": 41.03, + "workload": "graph-benchmark 20k q2" + }, + "graphbench.20k.q2.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 44.41, + "workload": "graph-benchmark 20k q2" + }, + "graphbench.20k.q2.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 67.74, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.pandas": { @@ -445,11 +919,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 38.05, + "value": 30.16, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars": { @@ -459,13 +933,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 17.59, + "value": 11.05, "workload": "graph-benchmark 20k q2" }, + "graphbench.20k.q2.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q2.polars", + "graphbench.20k.q2.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 2.7, + "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q3.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -474,10 +965,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 5.68, + "workload": "graph-benchmark 20k q3" + }, + "graphbench.20k.q3.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 4.16, + "workload": "graph-benchmark 20k q3" + }, + "graphbench.20k.q3.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 5.94, + "value": 8.31, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.pandas": { @@ -487,11 +1006,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 11.68, + "value": 11.32, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars": { @@ -501,13 +1020,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 7.66, + "value": 4.69, "workload": "graph-benchmark 20k q3" }, + "graphbench.20k.q3.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q3.polars", + "graphbench.20k.q3.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 2.4, + "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q4.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -516,10 +1052,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 3.18, + "workload": "graph-benchmark 20k q4" + }, + "graphbench.20k.q4.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 6.38, + "workload": "graph-benchmark 20k q4" + }, + "graphbench.20k.q4.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 3.42, + "value": 11.54, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.pandas": { @@ -529,11 +1093,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 9.45, + "value": 9.84, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars": { @@ -543,13 +1107,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 6.04, + "value": 3.23, "workload": "graph-benchmark 20k q4" }, + "graphbench.20k.q4.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q4.polars", + "graphbench.20k.q4.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 3.0, + "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q5.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -558,10 +1139,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 5.18, + "workload": "graph-benchmark 20k q5" + }, + "graphbench.20k.q5.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 4.63, + "workload": "graph-benchmark 20k q5" + }, + "graphbench.20k.q5.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 5.75, + "value": 18.33, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.pandas": { @@ -571,11 +1180,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 81.03, + "value": 79.44, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars": { @@ -585,13 +1194,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 8.9, + "value": 4.2, "workload": "graph-benchmark 20k q5" }, + "graphbench.20k.q5.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q5.polars", + "graphbench.20k.q5.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 18.9, + "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q6.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -600,10 +1226,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 8.67, + "workload": "graph-benchmark 20k q6" + }, + "graphbench.20k.q6.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 8.23, + "value": 4.85, + "workload": "graph-benchmark 20k q6" + }, + "graphbench.20k.q6.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 20.99, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.pandas": { @@ -613,11 +1267,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 81.25, + "value": 79.42, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars": { @@ -627,13 +1281,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 10.43, + "value": 4.79, "workload": "graph-benchmark 20k q6" }, + "graphbench.20k.q6.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q6.polars", + "graphbench.20k.q6.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 16.6, + "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q7.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -642,10 +1313,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 5.13, + "workload": "graph-benchmark 20k q7" + }, + "graphbench.20k.q7.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 5.39, + "workload": "graph-benchmark 20k q7" + }, + "graphbench.20k.q7.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 5.19, + "value": 15.14, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.pandas": { @@ -655,11 +1354,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 19.97, + "value": 19.52, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars": { @@ -669,13 +1368,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 10.16, + "value": 3.25, "workload": "graph-benchmark 20k q7" }, + "graphbench.20k.q7.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q7.polars", + "graphbench.20k.q7.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 6.0, + "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q8.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -684,44 +1400,85 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 2.69, + "workload": "graph-benchmark 20k q8" + }, + "graphbench.20k.q8.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 198.8, + "workload": "graph-benchmark 20k q8" + }, + "graphbench.20k.q8.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 2.52, + "value": 198.71, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.pandas": { - "board_quotable": false, - "comparison_allowed": false, + "board_quotable": true, + "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, - "disclosures": [ - "Measured with a harness that reused ONE Plottable across iterations, so pygraphistry's cross-call degree-count memo (pygraphistry #1825) made every timed run a cache HIT. One-shot, the same query is 8.89 ms at 20k and 34.11 ms at 100k on polars -- a LOSS to Kuzu, not a win. Re-measured in results/graphbench-q8-binding-20260729/; the memo hits ALL engines, so the pandas figure is flattered too." - ], + "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 10.46, + "value": 11.5, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars": { - "board_quotable": false, - "comparison_allowed": false, + "board_quotable": true, + "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, - "disclosures": [ - "Measured with a harness that reused ONE Plottable across iterations, so pygraphistry's cross-call degree-count memo (pygraphistry #1825) made every timed run a cache HIT. One-shot, the same query is 8.89 ms at 20k and 34.11 ms at 100k on polars -- a LOSS to Kuzu, not a win. Re-measured in results/graphbench-q8-binding-20260729/; the memo hits ALL engines, so the pandas figure is flattered too." - ], + "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 2.07, + "value": 2.08, "workload": "graph-benchmark 20k q8" }, + "graphbench.20k.q8.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q8.polars", + "graphbench.20k.q8.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 5.5, + "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars vs GFQL-pandas" + }, "graphbench.20k.q9.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -730,10 +1487,38 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 10.68, + "workload": "graph-benchmark 20k q9" + }, + "graphbench.20k.q9.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 2, + "disclosures": [], + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "ms", + "value": 124.91, + "workload": "graph-benchmark 20k q9" + }, + "graphbench.20k.q9.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 2, + "disclosures": [], + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 10.85, + "value": 422.29, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.pandas": { @@ -743,11 +1528,11 @@ "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 28.29, + "value": 22.35, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars": { @@ -757,13 +1542,30 @@ "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260813", "status": "ok", "unit": "ms", - "value": 13.83, + "value": 8.81, "workload": "graph-benchmark 20k q9" }, + "graphbench.20k.q9.polars_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q9.polars", + "graphbench.20k.q9.pandas" + ], + "run": "graphbench-q1q9-20k-20260813", + "status": "ok", + "unit": "x", + "value": 2.5, + "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" + }, "pagerank.gplus.gfql_cpu": { "board_quotable": true, "comparison_allowed": true, @@ -880,8 +1682,322 @@ "value": 11.72, "workload": "filter -> PageRank -> filter, twitter \u2014 Neo4j + GDS" }, - "snb.sf1.is3.single_query.polars": { - "board_quotable": false, + "snb.sf01.message_content.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.91, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" + }, + "snb.sf01.message_content.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.717, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" + }, + "snb.sf01.message_content.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.486, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" + }, + "snb.sf01.message_content.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.26, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" + }, + "snb.sf01.message_creator.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 20.352, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" + }, + "snb.sf01.message_creator.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 1.433, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" + }, + "snb.sf01.message_creator.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.603, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" + }, + "snb.sf01.message_creator.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.63, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" + }, + "snb.sf01.message_replies.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 48.017, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-replies" + }, + "snb.sf01.message_replies.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 23.904, + "workload": "LDBC SNB-derived aligned queries sf0.1 message-replies" + }, + "snb.sf01.new_topics.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 45.86, + "workload": "LDBC SNB-derived aligned queries sf0.1 new-topics" + }, + "snb.sf01.new_topics.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 47.198, + "workload": "LDBC SNB-derived aligned queries sf0.1 new-topics" + }, + "snb.sf01.recent_replies.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 84.978, + "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" + }, + "snb.sf01.recent_replies.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 32.407, + "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" + }, + "snb.sf01.recent_replies.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 4.342, + "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" + }, + "snb.sf01.recent_replies.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 7.454, + "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" + }, + "snb.sf01.seed_lookup.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 26.706, + "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" + }, + "snb.sf01.seed_lookup.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.329, + "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" + }, + "snb.sf01.seed_lookup.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.592, + "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" + }, + "snb.sf01.seed_lookup.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 3.497, + "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" + }, + "snb.sf1.is3.single_query.polars": { + "board_quotable": false, "comparison_allowed": false, "decimals": 2, "disclosures": [ @@ -909,12 +2025,238 @@ "unit": "ms", "value": 75.22, "workload": "LDBC SNB IS3 (SF1), superseded two-pass adapter form" + }, + "snb.sf1.message_content.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 3.556, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" + }, + "snb.sf1.message_content.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.674, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" + }, + "snb.sf1.message_content.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.293, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" + }, + "snb.sf1.message_content.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.435, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" + }, + "snb.sf1.message_creator.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 78.38, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" + }, + "snb.sf1.message_creator.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 1.509, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" + }, + "snb.sf1.message_creator.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.308, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" + }, + "snb.sf1.message_creator.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 1.671, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" + }, + "snb.sf1.new_topics.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 201.442, + "workload": "LDBC SNB-derived aligned queries sf1 new-topics" + }, + "snb.sf1.new_topics.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 223.715, + "workload": "LDBC SNB-derived aligned queries sf1 new-topics" + }, + "snb.sf1.seed_lookup.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 89.161, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" + }, + "snb.sf1.seed_lookup.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.517, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" + }, + "snb.sf1.seed_lookup.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "memgraph", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 0.523, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" + }, + "snb.sf1.seed_lookup.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, + "disclosures": [ + "Matched SNB-derived query shapes on the SNB Interactive dataset, run without the official LDBC driver; this is internal evidence, not an audited or official LDBC result. Each engine ran under the same timing contract with exact result parity, and the three graph databases are faster than GFQL on every universal point-lookup cell." + ], + "engine": "neo4j", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", + "status": "ok", + "unit": "ms", + "value": 2.884, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" } }, "contract_version": 3, - "generated_at": "2026-09-04T00:02:28Z", - "generated_by": "graphistry/pyg-bench c0e15bb2 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T05:28:48Z", + "generated_by": "graphistry/pyg-bench 3beae394 scripts/export_docs_numbers.py", "policy": { + "drift_waivers": { + "filter-pagerank-20260728": "GFQL CPU/GPU arms measured at pygraphistry 49db91cc (411 graphistry/compute commits before 0.59.0); the Neo4j GPlus arm was re-measured in the locked 2026-08-30 lane. A release-commit re-measurement of the GFQL arms is tracked.", + "graphbench-q1q9-100k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", + "graphbench-q1q9-20k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", + "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." + }, "literal_allowlist": {}, "managed_docs": [ "gfql/performance.rst", @@ -965,35 +2307,50 @@ "row_validation": "All 12 selected-node sets had Jaccard 1.0 against the reference (gate 0.95); every slot passed typed result, exact run-contract, and load/self-spike validation; the committed aggregate exactly recomputes from all slots.", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13); GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-100k-20260726": { - "artifact": "results/graphbench-matched-q1q9-20260726/gb100k", - "competitor": "Kuzu", - "competitor_version": "0.11.3 (embedded, CPU)", + "graphbench-q1q9-100k-20260813": { + "artifact": "results/graphbench-board-relane-20260813/100k with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "competitor": "Kuzu, Memgraph, Neo4j", + "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 100,000 persons / 2,417,738 FOLLOWS / 2,775,195 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-07-26", + "measured_at": "2026-08-13", "perf_lock_held": true, - "pyg_bench_commit": "47f94ba", - "pygraphistry_commit": "84be35fb", + "pyg_bench_commit": "8ee0dee9", + "pygraphistry_commit": "4d301b18", "quiet_host": true, - "reps": "2 warmups + 5 timed runs per slot; slot median, then median across 4 position-balanced slots (K G G K K G G K)", - "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is only publishable when its rows equal Kuzu's on every slot", - "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu in a host venv on CPU; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" - }, - "graphbench-q1q9-20k-20260726": { - "artifact": "results/graphbench-matched-q1q9-20260726/gb20k", - "competitor": "Kuzu", - "competitor_version": "0.11.3 (embedded, CPU)", + "reps": "GFQL and Kuzu: 3 warmups then timed runs per slot, 4 position-balanced slots per engine (G/K), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included). Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + }, + "graphbench-q1q9-20k-20260813": { + "artifact": "results/graphbench-board-relane-20260813/20k with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "competitor": "Kuzu, Memgraph, Neo4j", + "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 20,000 persons / 199,939 FOLLOWS / 260,107 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-07-26", + "measured_at": "2026-08-12", + "perf_lock_held": true, + "pyg_bench_commit": "8ee0dee9", + "pygraphistry_commit": "4d301b18", + "quiet_host": true, + "reps": "GFQL and Kuzu: 3 warmups then timed runs per slot, 4 position-balanced slots per engine (G/K), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included). Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + }, + "snb-aligned-release-20260902": { + "artifact": "results/snb-aligned-release-20260902", + "competitor": "Kuzu, Neo4j, Memgraph", + "competitor_version": "Kuzu 0.11.3; Neo4j 5 community; Memgraph 2.18.1", + "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (SF10 is a capability boundary and is excluded)", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-02", "perf_lock_held": true, - "pyg_bench_commit": "47f94ba", - "pygraphistry_commit": "84be35fb", + "pyg_bench_commit": "15a6f3a7", + "pygraphistry_commit": "3fb216dd", "quiet_host": true, - "reps": "3 warmups + 7 timed runs per slot; slot median, then median across 4 position-balanced slots (K G I I G K K G I I G K)", - "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is only publishable when its rows equal Kuzu's on every slot", - "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu in a host venv on CPU; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", + "row_validation": "exact result parity for every measured query across all engines; cells with zero rows, reduced adapters, or missing queries are excluded", + "runtime": "GFQL Polars on CPU in a uv Python 3.12 container; Kuzu 0.11.3; Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker); all on the same host" }, "snb-is3-single-query-20260727": { "artifact": "results/is3-single-query-r{1,2,3} and results/is3-twopass-base-r{1,2,3}", diff --git a/docs/source/_ext/gfql_bench.py b/docs/source/_ext/gfql_bench.py index 82b595b657..73d6388ca3 100644 --- a/docs/source/_ext/gfql_bench.py +++ b/docs/source/_ext/gfql_bench.py @@ -34,6 +34,8 @@ from gfql_bench_data import ( BenchDataError, + format_tally, + tally, JSONObject, State, audit_pages, @@ -78,6 +80,26 @@ def role(name: str, rawtext: str, key: str, lineno: int, inliner: Inliner, return role +def _tally_role(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, + options=None, content=None) -> RoleResult: + """``:bench-tally:`||``` -> "N of M" from published cells.""" + state = _state() + docname = inliner.document.settings.env.docname + parts = [part.strip() for part in text.split('|')] + before = len(state.problems) + result = None + if len(parts) != 3 or not all(parts): + state.fail('{}:{}: bench-tally expects ||, got {!r}'.format( + docname, lineno, text)) + else: + result = tally(state, parts[0], parts[1], parts[2], docname, lineno) + for message in state.problems[before:]: + logger.warning('[gfql-bench] %s', message) + if result is None: + return [nodes.strong(rawtext, '[MISSING BENCHMARK TALLY: {}]'.format(text))], [] + return [nodes.Text(format_tally(*result))], [] + + class BenchProvenance(Directive): """Render the run records behind the numbers on this page as one block. @@ -265,6 +287,7 @@ def _on_build_finished(app: Sphinx, exception: Optional[Exception]) -> None: def setup(app: Sphinx) -> Dict[str, object]: app.add_role('bench', _bench_role(diagnostic=False)) app.add_role('bench-diag', _bench_role(diagnostic=True)) + app.add_role('bench-tally', _tally_role) app.add_directive('bench-provenance', BenchProvenance) app.add_directive('bench-disclosures', BenchDisclosures) app.connect('builder-inited', _on_builder_inited) diff --git a/docs/source/_ext/gfql_bench_data.py b/docs/source/_ext/gfql_bench_data.py index c188d6f414..e1ba4f4880 100644 --- a/docs/source/_ext/gfql_bench_data.py +++ b/docs/source/_ext/gfql_bench_data.py @@ -19,6 +19,7 @@ import math import os import re +import subprocess import typing #: A decoded JSON document. @@ -266,6 +267,14 @@ def __init__(self, payload: JSONObject, today: datetime.date) -> None: policy = _obj(payload.get('policy'), 'policy') max_age = policy.get('max_age_days') self.max_age_days = max_age if isinstance(max_age, int) and not isinstance(max_age, bool) else 0 + drift = policy.get('max_compute_commit_drift') + self.max_compute_commit_drift = ( + drift if isinstance(drift, int) and not isinstance(drift, bool) else None) + waivers = policy.get('drift_waivers') + self.drift_waivers: typing.Dict[str, str] = { + run_id: reason for run_id, reason in (waivers.items() if isinstance(waivers, dict) else []) + if isinstance(reason, str) and reason} + self.drift_by_run: typing.Dict[str, typing.Optional[int]] = {} self.cells = _obj(payload.get('cells'), 'cells') self.runs = _obj(payload.get('runs'), 'runs') @@ -286,6 +295,15 @@ def run(self, run_id: str) -> typing.Optional[JSONObject]: raw = self.runs.get(run_id) return raw if isinstance(raw, dict) else None + def drift(self, run_id: str) -> typing.Optional[int]: + """Compute-commit drift of a run, computed once per build; None when unknowable.""" + if run_id not in self.drift_by_run: + run = self.run(run_id) + commit = run.get('pygraphistry_commit') if run is not None else None + self.drift_by_run[run_id] = ( + compute_commit_drift(commit) if isinstance(commit, str) else None) + return self.drift_by_run[run_id] + def age_days(self, run_id: str) -> typing.Optional[int]: run = self.run(run_id) if run is None: @@ -335,6 +353,13 @@ def check_reference(state: State, key: str, docname: str, lineno: int, state.fail('{}:{}: {!r} was measured {} days ago; policy.max_age_days is {}. ' 'Re-measure in pyg-bench and republish.'.format( docname, lineno, key, age, state.max_age_days)) + limit = state.max_compute_commit_drift + waived = run_id in state.drift_waivers + drift = state.drift(run_id) if limit is not None and not waived else None + if drift is not None and drift > limit: + state.fail('{}:{}: {!r} was measured {} graphistry/compute commits ago; ' + 'policy.max_compute_commit_drift is {}. Re-measure in pyg-bench ' + 'and republish.'.format(docname, lineno, key, drift, limit)) quotable = cell['board_quotable'] is True if diagnostic and quotable: @@ -372,6 +397,67 @@ def audit_pages(state: State) -> None: '".. bench-disclosures::" block'.format(docname)) +#: Repository root: ``docs/source/_data`` is three levels below it. +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(DATA_DIR))) +COMPUTE_PATH = 'graphistry/compute' + + +def compute_commit_drift(measured_commit: str, repo_root: str = REPO_ROOT) -> typing.Optional[int]: + """Commits touching ``graphistry/compute`` between a measurement and this checkout. + + ``None`` when git or the measured commit is unavailable (a shallow clone), in which + case the caller must not treat the number as fresh either way. + """ + if not re.match(r'^[0-9a-f]{7,40}$', measured_commit): + return None + try: + completed = subprocess.run( + ['git', 'rev-list', '--count', '{}..HEAD'.format(measured_commit), '--', COMPUTE_PATH], + cwd=repo_root, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False) + except OSError: + return None + if completed.returncode != 0: + return None + text = completed.stdout.decode().strip() + return int(text) if text.isdigit() else None + + +def tally(state: 'State', prefix: str, left: str, right: str, + docname: str, lineno: int) -> typing.Optional[typing.Tuple[int, int]]: + """How many of ``..`` beat ``..`` (lower is faster). + + Every cell the tally reads is registered as a reference, so the page owes the run's + provenance exactly as if it had printed the numbers. Ties are not wins. Returns None + (and records the problem) when no quotable pair exists. + """ + wins = 0 + total = 0 + for key in sorted(state.cells): + head, _, engine = key.rpartition('.') + if engine != left or not head.startswith(prefix + '.'): + continue + right_key = '{}.{}'.format(head, right) + left_cell = check_reference(state, key, docname, lineno, diagnostic=False) + right_cell = check_reference(state, right_key, docname, lineno, diagnostic=False) + if left_cell is None or right_cell is None: + continue + if left_cell.get('unit') != 'ms' or right_cell.get('unit') != 'ms': + continue + total += 1 + if float(left_cell['value']) < float(right_cell['value']): + wins += 1 + if total == 0: + state.fail('{}:{}: bench-tally {}|{}|{} matched no quotable cell pair'.format( + docname, lineno, prefix, left, right)) + return None + return wins, total + + +def format_tally(wins: int, total: int) -> str: + return '{} of {}'.format(wins, total) + + + def load_state(today: typing.Optional[datetime.date] = None) -> State: """Load the vendored artifact, re-verify it, and return the render-time state.""" payload = load(BENCHMARKS_JSON) diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index 9188217021..8ee9d2d968 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -32,6 +32,13 @@ GFQL is an in-process graph query language for the compute tier. With it you can ML, and the rest of the PyData stack apply directly. - **Move work to a remote GPU** when the data or the hardware lives elsewhere. +On CPU alone, GFQL's Polars engine is faster than Kuzu, Memgraph, and Neo4j on most of +the nine graph-benchmark analytics queries at both graph sizes, and the GPU engines are +faster still on heavy work; the graph databases win the SNB-derived point lookups. The +per-query boards, with every loss shown, are on :doc:`performance`, and a full filter, +PageRank, filter pipeline against Neo4j + GDS is the :doc:`speedup case study +`. + Typical uses include cybersecurity, fraud detection, financial analysis, and knowledge graphs. The engine design is described in the blog post `Cypher on Polars: a CPU and GPU graph engine `_; diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 6bf07bb856..384e898699 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -1,18 +1,21 @@ .. _gfql-performance: -GFQL Performance: Vectorization and GPU Acceleration -==================================================== +GFQL Performance: Measured Against Graph Databases +================================================== -This page collects measured GFQL performance results. See :doc:`index_adjacency` -for the adjacency index used by the seeded-lookup tests. +This page holds GFQL's measured performance results. Every number renders from a +committed pyg-bench artifact; the Measurement block at the end names the runs, hosts, +and commits. Losses appear next to wins. Choose an engine ---------------- GFQL runs the same query on ``pandas`` (the default), ``polars`` (CPU), ``cudf`` -(NVIDIA GPU), or ``polars-gpu``. The benchmark checks that each engine returns the -same rows. If an engine cannot run a query, GFQL reports an error before execution -instead of silently changing engines. +(NVIDIA GPU), or ``polars-gpu``. Each engine returns the same rows, or GFQL reports an +error before execution instead of changing engines. On the q1–q9 boards below, the +Polars engine is faster than pandas on :bench-tally:`graphbench.100k|polars|pandas` +queries at 100,000 people, by up to :bench:`graphbench.100k.q5.polars_vs_pandas` +(q5). See :doc:`engines` for the selection guide. .. doc-test: skip @@ -21,378 +24,327 @@ instead of silently changing engines. g.gfql(query) # engine='pandas' (default) g.gfql(query, engine='polars') # columnar CPU execution -On the LDBC Social Network Benchmark (SNB) SF1 seed lookup below, changing from -pandas to Polars reduced the time from **1,299.6 ms** to **106.1 ms**, or -**12.3×**, without a GPU. - -.. _gfql-0580-numbers: - -Measurements on version 0.58.0 ------------------------------- - -These warm medians use the **0.58.0 release tag** on an NVIDIA DGX Spark (GB10), -with 30 measured runs. Tests checked result rows across GFQL engines and against -the expected Neo4j and Kuzu results. - -Seeded typed-hop fast path -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A seeded typed hop starts at a known node and follows one relationship type. For -the query ``MATCH (m {id: ...})-[:T]->(p) RETURN p`` on a 50k-node, 200k-edge -graph, the fast path reduced the time on every engine: - -.. 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 an adjacency index -~~~~~~~~~~~~~~~~~~~~~~~ - -Building the optional in-memory adjacency index once with ``g.gfql_index_all()`` -reduced the same lookup to pandas **1.74 ms**, Polars **1.59 ms**, Polars GPU -**1.91 ms**, and cuDF **5.78 ms**. - -.. warning:: - For a Polars graph, build the index with - ``g.gfql_index_all(engine='polars')``. Automatic engine selection currently - converts the frames to pandas. PR #1767 tracks the fix. - -Lookup time as the graph grows -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -With the adjacency index, a seeded one-hop ``g.hop()`` on pandas took -**0.159–0.164 ms from 0.25M to 32M edges** at an average degree of four. The index -reads the seed's neighbors instead of scanning every edge. The Polars hop path does -not yet use this index. - -Compared with Neo4j (LDBC SNB interactive SF1) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -On the same host after warm-up, GFQL was faster on four of five queries: - -.. list-table:: - :header-rows: 1 - :widths: 30 20 20 15 - - * - 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 - -Neo4j was faster on ``recent-replies``. - -Analytical queries with multiple joins -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The OLAP multi-join comparison against an embedded graph database is the q1–q9 board -below: :ref:`gfql-vs-kuzu-board`. - -When not to use GFQL -~~~~~~~~~~~~~~~~~~~~ - -In the same tests, embedded Kuzu was **2–4× faster for single-table aggregates** -and **2.4–64× faster for lookups that return node properties**. GFQL performed -best on traversals, analytical queries with multiple joins, and indexed seeded -queries. Keep data in a database when the application needs durable shared storage. - .. _gfql-vs-kuzu-board: -The q1–q9 comparison: GFQL and Kuzu ------------------------------------ +The q1–q9 board: GFQL, Kuzu, Memgraph, and Neo4j +------------------------------------------------- -These tables cover nine Cypher queries from ``prrao87/graph-benchmark``. They rank -nodes by degree, group and filter records, and count two-hop paths on synthetic -social graphs with 20,000 and 100,000 people. Each table cell passed result-row -validation. +Nine Cypher queries from ``prrao87/graph-benchmark`` rank nodes by degree, group and +filter records, and count two-hop paths on synthetic social graphs with 20,000 and +100,000 people. Every cell passed result-row validation against every other engine. +Times are milliseconds; lower is better. -Kuzu parses and executes the query text on each call. GFQL reuses a prepared graph, -so the tables show direct times rather than Kuzu-to-GFQL speedup ratios. The q8 GFQL -cells are marked diagnostic because they reused cached degree data between calls; -do not use those cells as benchmark results. +GFQL binds the graph cold inside every timed run. Kuzu compiles the query text on each +call. Memgraph and Neo4j answer over Bolt with their default plan caches. These are +direct times under those profiles, not cross-engine speedup ratios. -See :doc:`benchmark_filter_pagerank` for the Neo4j filter → PageRank → filter -comparison. +At 20,000 people, GFQL Polars is faster than Kuzu on +:bench-tally:`graphbench.20k|polars|kuzu` queries, than Memgraph on +:bench-tally:`graphbench.20k|polars|memgraph`, and than Neo4j on +:bench-tally:`graphbench.20k|polars|neo4j`. At 100,000 people the counts are +:bench-tally:`graphbench.100k|polars|kuzu` (Kuzu), +:bench-tally:`graphbench.100k|polars|memgraph` (Memgraph), and +:bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Memgraph wins q5, q6, and q7 at +100,000 people: its planner starts from the ten-node interest side, which GFQL's +Cypher path does not yet do. The 20,000-person board ~~~~~~~~~~~~~~~~~~~~~~~ -The q8 GFQL cells are diagnostic; all other cells are benchmark results. - .. list-table:: :header-rows: 1 - :widths: 10 12 20 20 20 + :widths: 8 10 14 14 14 20 20 * - Query - - Result rows + - Rows - Kuzu + - Memgraph + - Neo4j - GFQL ``pandas`` - GFQL ``polars`` * - q1 - 3 - :bench:`graphbench.20k.q1.kuzu` + - :bench:`graphbench.20k.q1.memgraph` + - :bench:`graphbench.20k.q1.neo4j` - :bench:`graphbench.20k.q1.pandas` - :bench:`graphbench.20k.q1.polars` * - q2 - 1 - :bench:`graphbench.20k.q2.kuzu` + - :bench:`graphbench.20k.q2.memgraph` + - :bench:`graphbench.20k.q2.neo4j` - :bench:`graphbench.20k.q2.pandas` - :bench:`graphbench.20k.q2.polars` * - q3 - 5 - :bench:`graphbench.20k.q3.kuzu` + - :bench:`graphbench.20k.q3.memgraph` + - :bench:`graphbench.20k.q3.neo4j` - :bench:`graphbench.20k.q3.pandas` - :bench:`graphbench.20k.q3.polars` * - q4 - 2 - :bench:`graphbench.20k.q4.kuzu` + - :bench:`graphbench.20k.q4.memgraph` + - :bench:`graphbench.20k.q4.neo4j` - :bench:`graphbench.20k.q4.pandas` - :bench:`graphbench.20k.q4.polars` * - q5 - 1 - :bench:`graphbench.20k.q5.kuzu` + - :bench:`graphbench.20k.q5.memgraph` + - :bench:`graphbench.20k.q5.neo4j` - :bench:`graphbench.20k.q5.pandas` - :bench:`graphbench.20k.q5.polars` * - q6 - 5 - :bench:`graphbench.20k.q6.kuzu` + - :bench:`graphbench.20k.q6.memgraph` + - :bench:`graphbench.20k.q6.neo4j` - :bench:`graphbench.20k.q6.pandas` - :bench:`graphbench.20k.q6.polars` * - q7 - 1 - :bench:`graphbench.20k.q7.kuzu` + - :bench:`graphbench.20k.q7.memgraph` + - :bench:`graphbench.20k.q7.neo4j` - :bench:`graphbench.20k.q7.pandas` - :bench:`graphbench.20k.q7.polars` * - q8 - 1 - :bench:`graphbench.20k.q8.kuzu` - - :bench-diag:`graphbench.20k.q8.pandas` - - :bench-diag:`graphbench.20k.q8.polars` + - :bench:`graphbench.20k.q8.memgraph` + - :bench:`graphbench.20k.q8.neo4j` + - :bench:`graphbench.20k.q8.pandas` + - :bench:`graphbench.20k.q8.polars` * - q9 - 1 - :bench:`graphbench.20k.q9.kuzu` + - :bench:`graphbench.20k.q9.memgraph` + - :bench:`graphbench.20k.q9.neo4j` - :bench:`graphbench.20k.q9.pandas` - :bench:`graphbench.20k.q9.polars` The 100,000-person board ~~~~~~~~~~~~~~~~~~~~~~~~ -The same queries run on a graph with 100,000 people. - .. list-table:: :header-rows: 1 - :widths: 10 12 20 20 20 + :widths: 8 10 14 14 14 20 20 * - Query - - Result rows + - Rows - Kuzu + - Memgraph + - Neo4j - GFQL ``pandas`` - GFQL ``polars`` * - q1 - 3 - :bench:`graphbench.100k.q1.kuzu` + - :bench:`graphbench.100k.q1.memgraph` + - :bench:`graphbench.100k.q1.neo4j` - :bench:`graphbench.100k.q1.pandas` - :bench:`graphbench.100k.q1.polars` * - q2 - 1 - :bench:`graphbench.100k.q2.kuzu` + - :bench:`graphbench.100k.q2.memgraph` + - :bench:`graphbench.100k.q2.neo4j` - :bench:`graphbench.100k.q2.pandas` - :bench:`graphbench.100k.q2.polars` * - q3 - 5 - :bench:`graphbench.100k.q3.kuzu` + - :bench:`graphbench.100k.q3.memgraph` + - :bench:`graphbench.100k.q3.neo4j` - :bench:`graphbench.100k.q3.pandas` - :bench:`graphbench.100k.q3.polars` * - q4 - 3 - :bench:`graphbench.100k.q4.kuzu` + - :bench:`graphbench.100k.q4.memgraph` + - :bench:`graphbench.100k.q4.neo4j` - :bench:`graphbench.100k.q4.pandas` - :bench:`graphbench.100k.q4.polars` * - q5 - 1 - :bench:`graphbench.100k.q5.kuzu` + - :bench:`graphbench.100k.q5.memgraph` + - :bench:`graphbench.100k.q5.neo4j` - :bench:`graphbench.100k.q5.pandas` - :bench:`graphbench.100k.q5.polars` * - q6 - 5 - :bench:`graphbench.100k.q6.kuzu` + - :bench:`graphbench.100k.q6.memgraph` + - :bench:`graphbench.100k.q6.neo4j` - :bench:`graphbench.100k.q6.pandas` - :bench:`graphbench.100k.q6.polars` * - q7 - 1 - :bench:`graphbench.100k.q7.kuzu` + - :bench:`graphbench.100k.q7.memgraph` + - :bench:`graphbench.100k.q7.neo4j` - :bench:`graphbench.100k.q7.pandas` - :bench:`graphbench.100k.q7.polars` * - q8 - 1 - :bench:`graphbench.100k.q8.kuzu` - - :bench-diag:`graphbench.100k.q8.pandas` - - :bench-diag:`graphbench.100k.q8.polars` + - :bench:`graphbench.100k.q8.memgraph` + - :bench:`graphbench.100k.q8.neo4j` + - :bench:`graphbench.100k.q8.pandas` + - :bench:`graphbench.100k.q8.polars` * - q9 - 1 - :bench:`graphbench.100k.q9.kuzu` + - :bench:`graphbench.100k.q9.memgraph` + - :bench:`graphbench.100k.q9.neo4j` - :bench:`graphbench.100k.q9.pandas` - :bench:`graphbench.100k.q9.polars` -Provenance -~~~~~~~~~~ - -.. bench-provenance:: graphbench-q1q9-20k-20260726 - -.. bench-provenance:: graphbench-q1q9-100k-20260726 +.. _gfql-snb-aligned: -.. bench-disclosures:: +SNB-derived point and small-result queries: the databases win +------------------------------------------------------------- -The values come from committed `pyg-bench `_ -artifacts. The documentation build and ``docs/test_bench_numbers.py`` reject missing, -stale, or unpublished values. +Matched query shapes derived from the LDBC Social Network Benchmark (SNB) Interactive +workload, run on the SF0.1 and SF1 datasets without the official LDBC driver. This is +internal evidence, not an official LDBC result. All four engines ran under one timing +contract with exact result parity. Times are milliseconds. -.. _gfql-bulk-sweep: +Kuzu, Neo4j, and Memgraph are all faster than GFQL on every universal cell, and +Memgraph is fastest in every row. These are point lookups and small results, where a +database's index and per-call floor beat GFQL's per-call compile and row pipeline. +GFQL's strengths are the bulk shapes above and on the :doc:`speedup case study +`; choose a database when the workload is dominated by +point lookups. -Bulk engine comparison ----------------------- +SF0.1 +~~~~~ -These measurements predate version 0.58.0. They use the SNAP -**com-LiveJournal** (35M edges) and **com-Orkut** (117M edges) graphs. +.. list-table:: + :header-rows: 1 + :widths: 24 19 19 19 19 -The table shows the median time after warm-up for the same query and result on -each engine. Orkut has 3.1M nodes and 117M edges. + * - Query + - GFQL ``polars`` + - Kuzu + - Neo4j + - Memgraph + * - seed lookup + - :bench:`snb.sf01.seed_lookup.gfql_polars` + - :bench:`snb.sf01.seed_lookup.kuzu` + - :bench:`snb.sf01.seed_lookup.neo4j` + - :bench:`snb.sf01.seed_lookup.memgraph` + * - message content + - :bench:`snb.sf01.message_content.gfql_polars` + - :bench:`snb.sf01.message_content.kuzu` + - :bench:`snb.sf01.message_content.neo4j` + - :bench:`snb.sf01.message_content.memgraph` + * - message creator + - :bench:`snb.sf01.message_creator.gfql_polars` + - :bench:`snb.sf01.message_creator.kuzu` + - :bench:`snb.sf01.message_creator.neo4j` + - :bench:`snb.sf01.message_creator.memgraph` + * - recent replies + - :bench:`snb.sf01.recent_replies.gfql_polars` + - :bench:`snb.sf01.recent_replies.kuzu` + - :bench:`snb.sf01.recent_replies.neo4j` + - :bench:`snb.sf01.recent_replies.memgraph` + * - message replies (GFQL and Kuzu only) + - :bench:`snb.sf01.message_replies.gfql_polars` + - :bench:`snb.sf01.message_replies.kuzu` + - + - + * - new topics (GFQL and Kuzu only) + - :bench:`snb.sf01.new_topics.gfql_polars` + - :bench:`snb.sf01.new_topics.kuzu` + - + - + +SF1 +~~~ .. list-table:: :header-rows: 1 - :widths: 34 16 16 16 16 - - * - Workload (Orkut, 117M edges) - - ``pandas`` - - ``polars`` - - ``cudf`` - - ``polars-gpu`` - * - 1-hop from 10K seeds - - 2613 ms - - **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 - -- Polars CPU reduced the one-hop time from 2613 ms to 68 ms and the aggregation - time from 799 ms to 205 ms, without a GPU. -- Polars builds one lazy query plan. cuDF executes each step separately, so its GPU - launch and intermediate-frame costs are larger on these workloads. -- Polars GPU was fastest for the 10K-seed two-hop query and the aggregation. cuDF - was fastest for the 100K-seed query, which produced about 85M rows. -- On LiveJournal, the 10K-seed one-hop query took 1129 ms on pandas and 37 ms on - Polars. Across 10K, 100K, and 1M-edge samples, Polars became faster as the graph - grew. Pandas was faster only for a node filter that took less than 1 ms. - Reproducer: ``benchmarks/gfql/index_crossover_bench.py``. - -Method -~~~~~~ - -- Host: NVIDIA DGX Spark (GB10 Grace-Blackwell, unified memory), RAPIDS container - ``graphistry/test-rapids-official:26.02-gfql-polars``. -- Datasets: `SNAP `_ **com-LiveJournal** (35M edges), - **com-Orkut** (117M edges). -- Measurement: warm median after two warm-ups, with five timed runs on Orkut and - eight on LiveJournal. Each engine returned the same result rows. -- 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:** LadybugDB 0.18.1 and GFQL with Polars ran in one - session on the same host and generated 5M-node, 20M-edge graph. The queries use - `LadybugDB/kuzu-ladybug-benchmark `_ - and returned matching values. GFQL was faster for a full node scan (59.0 ms vs - 364.3 ms) and a 1,001-row range scan (5.1 ms vs 7.6 ms). LadybugDB was faster - for indexed point lookups and a cached relationship count. Reproducer: - ``benchmarks/gfql/bench_ladybug_cypher.py``. - -Engine choice depends on the workload. Polars usually became faster than pandas -as graphs grew past 10K edges, while pandas remained faster for operations under -1 ms. See :doc:`engines` for selection guidance. See -:doc:`benchmark_filter_pagerank` for the Neo4j pipeline comparison and -:doc:`benchmark_graphframes` for the Spark GraphFrames comparison. - -How GFQL is fast ----------------- - -GFQL joins tables of nodes and edges in batches instead of following one path at a -time. This lets dataframe engines process many records together. + :widths: 24 19 19 19 19 -GFQL stores graph data in columnar frames based on -`Apache Arrow `_. Polars combines operations into one lazy -plan. cuDF and Polars GPU run columnar operations in parallel on NVIDIA GPUs. GPUs -help most on large joins, queries that visit many nodes, and full-graph aggregation, -where the work outweighs the cost of starting GPU operations. + * - Query + - GFQL ``polars`` + - Kuzu + - Neo4j + - Memgraph + * - seed lookup + - :bench:`snb.sf1.seed_lookup.gfql_polars` + - :bench:`snb.sf1.seed_lookup.kuzu` + - :bench:`snb.sf1.seed_lookup.neo4j` + - :bench:`snb.sf1.seed_lookup.memgraph` + * - message content + - :bench:`snb.sf1.message_content.gfql_polars` + - :bench:`snb.sf1.message_content.kuzu` + - :bench:`snb.sf1.message_content.neo4j` + - :bench:`snb.sf1.message_content.memgraph` + * - message creator + - :bench:`snb.sf1.message_creator.gfql_polars` + - :bench:`snb.sf1.message_creator.kuzu` + - :bench:`snb.sf1.message_creator.neo4j` + - :bench:`snb.sf1.message_creator.memgraph` + * - new topics (GFQL and Kuzu only) + - :bench:`snb.sf1.new_topics.gfql_polars` + - :bench:`snb.sf1.new_topics.kuzu` + - + - + +Neo4j and Memgraph use a reduced adapter for one query, and one parameter returns zero +rows; those cells are excluded rather than estimated. SF10 was not run. + +Lookups from known nodes +------------------------ + +A query that starts from known node ids (a watchlist, a session) scans every edge by +default. The opt-in adjacency index turns that scan into a gather over the seeds' +neighbors, so its cost tracks the seeds rather than the graph, on every engine. This +lane has not yet been measured under the provenance-carrying harness used above, so +this page prints no figure for it; see :doc:`index_adjacency` for the design and +:doc:`indexing` for the lifecycle. + +How GFQL is fast, and when it is not +------------------------------------ -Start on CPU with no special hardware, and move to a GPU engine by changing one keyword when -the graph or result becomes large. See :doc:`engines` for selection guidance. +GFQL joins tables of nodes and edges in batches instead of following one path at a +time, over columnar frames based on `Apache Arrow `_. Polars +fuses the operations into one lazy plan and collects once; cuDF and Polars GPU run the +same columnar operations on NVIDIA GPUs. That favors bulk work: multi-join analytics, +frontier expansion from many seeds, and full-graph aggregation. It does not favor +single-row point lookups, where the per-call compile and row-pipeline floor dominates +and an indexed database answers in well under a millisecond, as the SNB tables show. + +Start on CPU with no special hardware, and move to a GPU engine by changing one +keyword when the graph or result becomes large. The :doc:`speedup case study +` measures a full filter, PageRank, filter pipeline on CPU +and GPU against Neo4j + GDS, and :doc:`benchmark_graphframes` measures filters, +traversals, and PageRank against Spark GraphFrames. .. note:: Same-path constraints (``where``) can be more expensive on dense graphs. Prefer selective per-step predicates and see :doc:`/gfql/where` for details. +Provenance +---------- + +Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, +which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` +reject missing, stale, or unpublished values. + +.. bench-provenance:: graphbench-q1q9-20k-20260813 graphbench-q1q9-100k-20260813 snb-aligned-release-20260902 + :disclosures: + Next steps ---------- -- **Choose an engine**: :doc:`engines` — the full decision matrix and qualitative guidance. -- **Selective lookups**: :doc:`index_adjacency` — the adjacency index used above. -- **End-to-end benchmark**: :doc:`benchmark_filter_pagerank` — CPU/GPU vs Neo4j+GDS. +- **Choose an engine**: :doc:`engines`. +- **Lookups from known nodes**: :doc:`index_adjacency` and :doc:`indexing`. +- **Speedup case study**: :doc:`benchmark_filter_pagerank`. - **Explore GFQL**: :ref:`10min-gfql`. **Get started**: :ref:`10min-pygraphistry`. -- **Ecosystem**: `Apache Arrow `_ and `NVIDIA RAPIDS `_. diff --git a/docs/test_bench_numbers.py b/docs/test_bench_numbers.py index e2e65b7f30..a9ce683b17 100644 --- a/docs/test_bench_numbers.py +++ b/docs/test_bench_numbers.py @@ -360,3 +360,79 @@ def test_the_rules_module_stays_importable_without_sphinx(): for forbidden in ('docutils', 'sphinx'): assert 'import {}'.format(forbidden) not in source assert 'from {}'.format(forbidden) not in source + + +def _tally_payload(**cells: float) -> bench.JSONObject: + """A minimal artifact: one run, ms cells under ``gb..``, all quotable.""" + out_cells: bench.JSONObject = {} + for name, value in cells.items(): + query, engine = name.split("__") + out_cells[f"gb.{query}.{engine}"] = { + "run": "r", "workload": "w", "engine": engine, "measurement_profile": "p", + "value": value, "unit": "ms", "decimals": 2, "status": "ok", + "comparison_allowed": True, "board_quotable": True, "disclosures": [], + } + return { + "policy": {"max_age_days": 60}, + "runs": {"r": {"measured_at": datetime.date.today().isoformat(), + "pygraphistry_commit": "0123456789ab"}}, + "cells": out_cells, + } + + +def test_tally_counts_strict_wins_and_registers_every_cell_it_read(): + state = bench.State(_tally_payload( + q1__polars=1.0, q1__kuzu=2.0, + q2__polars=2.0, q2__kuzu=2.0, + q3__polars=3.0, q3__kuzu=1.0), datetime.date.today()) + assert bench.tally(state, "gb", "polars", "kuzu", "doc", 1) == (1, 3) + assert state.problems == [] + assert sorted(state.refs["doc"]) == sorted([ + "gb.q1.polars", "gb.q1.kuzu", "gb.q2.polars", "gb.q2.kuzu", "gb.q3.polars", "gb.q3.kuzu"]) + assert bench.format_tally(1, 3) == "1 of 3" + + +def test_tally_with_no_quotable_pair_is_a_recorded_problem(): + state = bench.State(_tally_payload(q1__polars=1.0), datetime.date.today()) + assert bench.tally(state, "gb", "polars", "kuzu", "doc", 7) is None + assert any("bench-tally" in problem for problem in state.problems) + + +def test_a_run_that_drifted_past_the_policy_fails_unless_waived(monkeypatch): + payload = _tally_payload(q1__polars=1.0, q1__kuzu=2.0) + payload["policy"]["max_compute_commit_drift"] = 12 + monkeypatch.setattr(bench, "compute_commit_drift", lambda commit, repo_root=None: 274) + state = bench.State(payload, datetime.date.today()) + bench.check_reference(state, "gb.q1.polars", "doc", 1, diagnostic=False) + assert any("max_compute_commit_drift" in problem for problem in state.problems) + + payload["policy"]["drift_waivers"] = {"r": "release re-measurement tracked"} + waived = bench.State(payload, datetime.date.today()) + bench.check_reference(waived, "gb.q1.polars", "doc", 1, diagnostic=False) + assert waived.problems == [] + + +def test_unknown_drift_never_fails_the_build(monkeypatch): + payload = _tally_payload(q1__polars=1.0, q1__kuzu=2.0) + payload["policy"]["max_compute_commit_drift"] = 12 + monkeypatch.setattr(bench, "compute_commit_drift", lambda commit, repo_root=None: None) + state = bench.State(payload, datetime.date.today()) + bench.check_reference(state, "gb.q1.polars", "doc", 1, diagnostic=False) + assert state.problems == [] + + +def test_compute_commit_drift_rejects_a_malformed_commit(): + assert bench.compute_commit_drift("not a sha") is None + + +def test_every_vendored_run_is_within_drift_policy_or_waived(): + """The shipping artifact's own runs, against this checkout (skips on shallow clones).""" + state = bench.load_state() + limit = state.max_compute_commit_drift + assert isinstance(limit, int) + over = [] + for run_id in sorted(state.runs): + drift = state.drift(run_id) + if drift is not None and drift > limit and run_id not in state.drift_waivers: + over.append((run_id, drift)) + assert not over, f"runs past max_compute_commit_drift={limit} without a waiver: {over}" From f7e2d5cb12bf1b1324767340af7488d864dd1363 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 22:44:50 -0700 Subject: [PATCH 06/29] docs(gfql): perf hub label matches the performance page title Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/gfql/perf/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/gfql/perf/index.rst b/docs/source/gfql/perf/index.rst index 2566848abe..b9351e1bed 100644 --- a/docs/source/gfql/perf/index.rst +++ b/docs/source/gfql/perf/index.rst @@ -18,7 +18,7 @@ pipeline with Neo4j + GDS. :maxdepth: 1 Choosing an Engine <../engines> - CPU and GPU Acceleration <../performance> + Measured Performance <../performance> Indexing Guide <../indexing> Adjacency Index for Lookups from Known Nodes <../index_adjacency> ../remote From 22474c521167f71e81a7acabf95dc6b9a16b70a7 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Thu, 3 Sep 2026 23:07:22 -0700 Subject: [PATCH 07/29] docs(gfql): case study renders the 0.59.0 GFQL arms; GPlus GPU cell is diagnostic - vendor pyg-bench published numbers with results/filter-pagerank-059-20260904 (GFQL CPU/GPU re-measured at 3fb216dd under the pinned PageRank contract) - lede states CPU-vs-Neo4j on both graphs and the Twitter GPU ratio; the GPlus GPU time renders via :bench-diag: with the selection caveat, and no GPlus GPU-vs-CPU ratio is claimed; alt texts follow the cells - gplus_pipeline chart drops the withdrawn ratio annotation Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- CHANGELOG.md | 1 + docs/source/_data/gfql_benchmarks.json | 73 ++++++++----------- docs/source/_ext/gfql_bench_charts.py | 6 +- .../filter_pagerank/gplus_pipeline.svg | 10 +-- .../filter_pagerank/twitter_pipeline.svg | 8 +- .../source/gfql/benchmark_filter_pagerank.rst | 41 ++++++----- 6 files changed, 66 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99bc262fe1..1529ef4f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. - **The GFQL benchmark pages state the result first and consolidate caveats**: `gfql/benchmark_filter_pagerank` (now titled as a case study and listed under Start Here) and `gfql/benchmark_graphframes` open with what the page compares and the measured outcome (GraphFrames: GFQL's best engine faster in all eight cells, CPU alone in six), keep every number unchanged, and move measurement rules into one "Method and limits" section per page. The GraphFrames page gains per-task bar charts rendered from `gfql/_static/graphframes/results.json` by the same generator and sync test as the Neo4j charts. The `bench-provenance` directive now accepts several run ids and renders one Measurement block with caveats folded in; run provenance strings name the RAPIDS base image (pyg-bench #220). +- **The speedup case study renders GFQL arms re-measured at the 0.59.0 release commit**: the vendored artifact now carries the 2026-09-04 filter/PageRank GFQL cells (Twitter CPU 2.62 s, GPU 0.23 s; GPlus CPU 34.11 s, which selects exactly the locked Neo4j lane's node set). The GPlus GPU arm selects a different node set than the CPU arm (Jaccard 0.91), so its time renders as a diagnostic and no GPlus GPU-vs-CPU ratio is published; the Twitter ratio is 11.3x. The pandas pipeline slowdown versus the July measurement is attributed by a same-harness A/B (#2021) and the cuGraph/igraph tail difference is tracked in #2022. - **GFQL performance page renders the relaned four-engine q1–q9 board and the aligned SNB matrix**: the vendored pyg-bench artifact now carries the 2026-08-13 board (GFQL bound cold per timed run) with Kuzu, Memgraph, and Neo4j columns and the SNB-derived SF0.1/SF1 four-engine tables, where the databases win every point-lookup cell. `gfql/performance` drops its release-pinned heading and every hand-typed legacy figure (typed-hop fast path, index timings, the old Neo4j five-query table, the bulk engine sweep, LadybugDB) in favor of published cells, one Measurement block, and win counts derived from cells by a new `:bench-tally:` role. The bench extension now enforces `policy.max_compute_commit_drift` against the checkout (git history permitting) with per-run waivers carried in the artifact, so a board measured long before the current compute code fails the build instead of aging silently. `gfql/overview` states where GFQL wins and where the databases win, with links. - **GFQL docs wording and structure pass**: Start Here is now the 10-minute guide, the overview, and the speedup case study; the quick reference and Cypher syntax guide head the Language Reference hub. `gfql/indexing` and `gfql/index_adjacency` carry plain-language titles. The engines page folds its opening note boxes into prose and its "coming from" table names the concrete change and where the measurement lives (Memgraph added, PuppyGraph dropped). `gfql/about` examples 3–7 gain tested Cypher twins, example 4 now returns rows on its own sample graph, and the sample-graph block runs in the doc-example lane so later examples are checked against it. Marketing filler ("leverage", "seamless", "honest") is removed across the GFQL pages. - **GFQL overview states the positioning up front**: `gfql/overview` says GFQL is the only open-source in-process Cypher on dataframes and frames it against the columnar engines readers already know. An empty hidden toctree that rendered the page as a folder is gone. diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 1cc4e53140..6772f6db7d 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -1573,42 +1573,27 @@ "disclosures": [], "engine": "GFQL CPU (pandas + igraph)", "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "s", - "value": 32.1, + "value": 34.11, "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL CPU (pandas + igraph)" }, "pagerank.gplus.gfql_gpu": { - "board_quotable": true, - "comparison_allowed": true, + "board_quotable": false, + "comparison_allowed": false, "decimals": 2, - "disclosures": [], + "disclosures": [ + "Its selected-node set differs from the GFQL CPU (pandas + igraph) arm's on gplus (Jaccard 0.91065, gate 0.95); this is a direct pipeline time, not a comparison." + ], "engine": "GFQL GPU (cuDF + cuGraph)", "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "s", - "value": 2.42, + "value": 2.47, "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL GPU (cuDF + cuGraph)" }, - "pagerank.gplus.gfql_gpu_vs_gfql_cpu": { - "board_quotable": true, - "comparison_allowed": true, - "decimals": 1, - "disclosures": [], - "engine": "GFQL GPU (cuDF + cuGraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "operands": [ - "pagerank.gplus.gfql_gpu", - "pagerank.gplus.gfql_cpu" - ], - "run": "filter-pagerank-20260728", - "status": "ok", - "unit": "x", - "value": 13.3, - "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL GPU (cuDF + cuGraph) vs GFQL CPU (pandas + igraph)" - }, "pagerank.gplus.neo4j_gds": { "board_quotable": true, "comparison_allowed": true, @@ -1631,10 +1616,10 @@ "disclosures": [], "engine": "GFQL CPU (pandas + igraph)", "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "s", - "value": 1.58, + "value": 2.62, "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL CPU (pandas + igraph)" }, "pagerank.twitter.gfql_gpu": { @@ -1644,10 +1629,10 @@ "disclosures": [], "engine": "GFQL GPU (cuDF + cuGraph)", "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "s", - "value": 0.24, + "value": 0.23, "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph)" }, "pagerank.twitter.gfql_gpu_vs_gfql_cpu": { @@ -1661,10 +1646,10 @@ "pagerank.twitter.gfql_gpu", "pagerank.twitter.gfql_cpu" ], - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "x", - "value": 6.7, + "value": 11.3, "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph) vs GFQL CPU (pandas + igraph)" }, "pagerank.twitter.neo4j_gds": { @@ -1672,11 +1657,12 @@ "comparison_allowed": true, "decimals": 2, "disclosures": [ - "In the Neo4j arm each filter stage writes a marker property onto every matched node and relationship, 32 seeds per batch, and GDS scores an explicitly symmetrised projection; the GDS in-memory projection is rebuilt per timed iteration while the GFQL arms retain their resident frames. The GFQL arms materialise a subgraph and write nothing. The figure is a pipeline time, not an engine-primitive time." + "In the Neo4j arm each filter stage writes a marker property onto every matched node and relationship, 32 seeds per batch, and GDS scores an explicitly symmetrised projection; the GDS in-memory projection is rebuilt per timed iteration while the GFQL arms retain their resident frames. The GFQL arms materialise a subgraph and write nothing. The figure is a pipeline time, not an engine-primitive time.", + "The Twitter Neo4j + GDS arm was measured on 2026-07-28 and is reused unchanged; the GFQL arms were re-measured on 2026-09-04 at PyGraphistry 0.59.0 under the pinned PageRank contract." ], "engine": "Neo4j + GDS", "measurement_profile": "filter-pagerank-server-roundtrip-projection-per-iteration", - "run": "filter-pagerank-20260728", + "run": "filter-pagerank-059-20260904", "status": "ok", "unit": "s", "value": 11.72, @@ -2248,11 +2234,10 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T05:28:48Z", - "generated_by": "graphistry/pyg-bench 3beae394 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T06:03:03Z", + "generated_by": "graphistry/pyg-bench 48a65e2d scripts/export_docs_numbers.py", "policy": { "drift_waivers": { - "filter-pagerank-20260728": "GFQL CPU/GPU arms measured at pygraphistry 49db91cc (411 graphistry/compute commits before 0.59.0); the Neo4j GPlus arm was re-measured in the locked 2026-08-30 lane. A release-commit re-measurement of the GFQL arms is tracked.", "graphbench-q1q9-100k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", "graphbench-q1q9-20k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." @@ -2277,20 +2262,20 @@ "max_compute_commit_drift": 12 }, "runs": { - "filter-pagerank-20260728": { - "artifact": "results/filter-pagerank-20260728", + "filter-pagerank-059-20260904": { + "artifact": "results/filter-pagerank-059-20260904 (GFQL arms) with the Twitter Neo4j arm copied from results/filter-pagerank-20260728", "competitor": "Neo4j + GDS", "competitor_version": "neo4j:2026.02.2 with the graph-data-science plugin", "dataset": "SNAP twitter_combined (81,306 nodes / 2,420,766 edges) and gplus_combined (107,614 nodes / 30,494,866 edges); sha256 of each source file is recorded in the arm artifacts", - "host": "dgx-spark (NVIDIA GB10, driver 580.126.09), 20 CPU", - "measured_at": "2026-07-28", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-04", "perf_lock_held": true, - "pyg_bench_commit": "85c92022 plus benchmarks/filter_pagerank as added in this commit", - "pygraphistry_commit": "49db91cc", + "pyg_bench_commit": "7426456", + "pygraphistry_commit": "3fb216dd", "quiet_host": true, - "reps": "graph loaded once, then 2 warmups + 5 timed runs per arm on the resident graph; median", - "row_validation": "Every arm records the node id set its pipeline selected, captured outside the timed region. Comparability is the Jaccard index of those sets against a 0.95 threshold declared before the run: Twitter CPU/GPU 0.991, CPU/Neo4j 0.974, GPU/Neo4j 0.972; GPlus CPU/GPU 0.951.", - "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13) with python-igraph 1.0.0; cuDF 26.2.1, cuGraph 26.2.0, pandas 2.3.3; Neo4j 2026.02.2 + graph-data-science in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + "reps": "GFQL arms: graph loaded once, then 2 warmups + 5 timed runs per arm on the resident graph, median; Twitter Neo4j + GDS arm carried over unchanged from the 2026-07-28 lane (2 warmups + 5 timed runs)", + "row_validation": "Every arm records the node id set its pipeline selected, captured outside the timed region. Comparability is the Jaccard index of those sets against a 0.95 threshold declared before the run; values are in summary.json (Twitter CPU/Neo4j 0.9999).", + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13) with python-igraph 1.0.0; cuDF 26.2.1, cuGraph 26.2.0, pandas 2.3.3; PageRank contract pinned (undirected, damping 0.85, cuGraph max_iter 100 / tol 1e-6); Twitter Neo4j 2026.02.2 + graph-data-science in Docker on the same host (2026-07-28); benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "filter-pagerank-gplus-locked-20260830": { "artifact": "results/gplus-locked-baseline-20260830", diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index edb63a26a2..95d3448b7d 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -108,10 +108,10 @@ class Chart(NamedTuple): bars=( Bar('Neo4j + GDS', 'neo', 'pagerank.gplus.neo4j_gds'), Bar('GFQL Cypher on CPU (pandas + igraph)', 'cpu', 'pagerank.gplus.gfql_cpu'), - Bar('GFQL Cypher on GPU (cuDF + cuGraph)', 'gpu', 'pagerank.gplus.gfql_gpu', - 'pagerank.gplus.gfql_gpu_vs_gfql_cpu', 'faster than the CPU path'), + Bar('GFQL Cypher on GPU (cuDF + cuGraph), diagnostic', 'gpu', + 'pagerank.gplus.gfql_gpu'), ), - foot='Direct timings span runs/profiles; only the GFQL GPU/CPU ratio is valid.', + foot='Direct timings span runs/profiles; the GPU arm selects a different node set.', )), )) diff --git a/docs/source/gfql/_static/filter_pagerank/gplus_pipeline.svg b/docs/source/gfql/_static/filter_pagerank/gplus_pipeline.svg index 1c28241ebe..0511a1cc2e 100644 --- a/docs/source/gfql/_static/filter_pagerank/gplus_pipeline.svg +++ b/docs/source/gfql/_static/filter_pagerank/gplus_pipeline.svg @@ -10,10 +10,10 @@ 354.47 s GFQL Cypher on CPU (pandas + igraph) - -32.10 s -GFQL Cypher on GPU (cuDF + cuGraph) + +34.11 s +GFQL Cypher on GPU (cuDF + cuGraph), diagnostic -2.42 s13.3x faster than the CPU path -Direct timings span runs/profiles; only the GFQL GPU/CPU ratio is valid. +2.47 s +Direct timings span runs/profiles; the GPU arm selects a different node set. diff --git a/docs/source/gfql/_static/filter_pagerank/twitter_pipeline.svg b/docs/source/gfql/_static/filter_pagerank/twitter_pipeline.svg index 4b61dd22e7..8869702a6b 100644 --- a/docs/source/gfql/_static/filter_pagerank/twitter_pipeline.svg +++ b/docs/source/gfql/_static/filter_pagerank/twitter_pipeline.svg @@ -10,10 +10,10 @@ 11.72 s GFQL Cypher on CPU (pandas + igraph) - -1.58 s + +2.62 s GFQL Cypher on GPU (cuDF + cuGraph) - -0.24 s6.7x faster than the GFQL CPU path + +0.23 s11.3x faster than the GFQL CPU path Direct timings use different profiles; only the GFQL GPU/CPU ratio is valid. diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index cca3574461..c17cb11584 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -10,13 +10,16 @@ This case study runs one three-stage graph pipeline, filter, PageRank, filter, on two systems. GFQL is Graphistry's open-source graph query language: Cypher that executes in-process on Python dataframes with no database. Neo4j + Graph Data Science (GDS) is the graph database and its analytics library. On both -graphs, Twitter (2.4M edges) and GPlus (30M edges), GFQL finished the pipeline -faster than Neo4j + GDS, on CPU and on GPU. On GPlus the GFQL GPU path takes -:bench:`pagerank.gplus.gfql_gpu`, the GFQL CPU path -:bench:`pagerank.gplus.gfql_cpu`, and Neo4j + GDS -:bench:`pagerank.gplus.neo4j_gds`. Use the GPU engine when one is available: -it is :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` faster than the CPU engine -on GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. +graphs, Twitter (2.4M edges) and GPlus (30M edges), GFQL on CPU finished the +pipeline faster than Neo4j + GDS while selecting the same nodes. On GPlus the +GFQL CPU path takes :bench:`pagerank.gplus.gfql_cpu` and Neo4j + GDS +:bench:`pagerank.gplus.neo4j_gds`. On Twitter the GFQL GPU path is +:bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster than the CPU path +(:bench:`pagerank.twitter.gfql_gpu` versus :bench:`pagerank.twitter.gfql_cpu`). +On GPlus the GPU path takes :bench-diag:`pagerank.gplus.gfql_gpu`, but its +cuGraph PageRank selects a different node set than igraph at the 0.9995 +cutoff, so that time is a diagnostic and no GPU-vs-CPU ratio is published +for GPlus. .. list-table:: :header-rows: 1 @@ -35,14 +38,14 @@ on GPlus and :bench:`pagerank.twitter.gfql_gpu_vs_gfql_cpu` faster on Twitter. * - **GPlus** (107,614 nodes / 30M edges) - :bench:`pagerank.gplus.neo4j_gds` - :bench:`pagerank.gplus.gfql_cpu` - - :bench:`pagerank.gplus.gfql_gpu` - - :bench:`pagerank.gplus.gfql_gpu_vs_gfql_cpu` + - :bench-diag:`pagerank.gplus.gfql_gpu` + - not published (selection differs) .. image:: _static/filter_pagerank/twitter_pipeline.svg - :alt: Twitter warm pipeline time: Neo4j + GDS 11.72s, GFQL Cypher CPU 1.58s, GFQL Cypher GPU 0.24s + :alt: Twitter warm pipeline time: Neo4j + GDS 11.72 s, GFQL Cypher CPU 2.62 s, GFQL Cypher GPU 0.23 s .. image:: _static/filter_pagerank/gplus_pipeline.svg - :alt: GPlus warm pipeline time: Neo4j + GDS 354.47s, GFQL Cypher CPU 32.10s, GFQL Cypher GPU 2.42s + :alt: GPlus warm pipeline time: Neo4j + GDS 354.47 s, GFQL Cypher CPU 34.11 s, GFQL Cypher GPU 2.47 s (diagnostic) The pipeline ------------ @@ -136,11 +139,15 @@ Method and limits ----------------- - **Workload**: one pipeline (filter, PageRank, filter) on two SNAP graphs. - In the GPlus locked run, the Neo4j and GFQL arms selected exactly the same - node set. -- **Timing**: warm runs after warm-up. The GPlus Neo4j time comes from a - later locked run of twelve position-balanced slots on one machine; the - Measurement block below records both runs. + Selected-node parity is measured (Jaccard, gate 0.95): on Twitter the GFQL + CPU arm matches Neo4j at 0.9999; on GPlus the GFQL CPU arm selects exactly + the locked lane's set; the GPlus GPU arm is at 0.91 against CPU and is + therefore diagnostic-only (see the caveats below). +- **Timing**: warm runs after warm-up (2 warm-ups, 5 timed runs, median). The + GFQL arms were measured at the 0.59.0 release commit; the Twitter Neo4j arm + is the 2026-07-28 measurement, and the GPlus Neo4j time comes from a later + locked run of twelve position-balanced slots on one machine. The Measurement + block below records every run. - **Profiles differ**: GFQL reuses frames already resident in Python. Neo4j includes server round trips, writes marker properties in both filter stages, and rebuilds the GDS in-memory projection on every timed iteration. The @@ -161,5 +168,5 @@ Provenance Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, which pyg-bench publishes. -.. bench-provenance:: filter-pagerank-20260728 filter-pagerank-gplus-locked-20260830 +.. bench-provenance:: filter-pagerank-059-20260904 filter-pagerank-gplus-locked-20260830 :disclosures: From 41b3490b002da82a8f94d8041734fe6e8fa9ebe0 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 00:06:52 -0700 Subject: [PATCH 08/29] docs(gfql): q1-q9 board re-measured at 0.59.0 with a polars-gpu column Vendor the pyg-bench publication (graphbench-q1q9-{20k,100k}-20260904: pandas, polars, polars-gpu at 3fb216dd; Memgraph/Neo4j lanes of 2026-08-12). performance.rst gains the polars-gpu column, tallies for GPU-vs-CPU and GFQL-vs-each-database, and names every loss (Kuzu q4/q8, Memgraph q3/q5/q6/q7, Neo4j q5, GPU q8). Provenance block retargeted to the new runs. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 944 ++++++++++++++++++++----- docs/source/gfql/performance.rst | 49 +- 2 files changed, 788 insertions(+), 205 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 6772f6db7d..f69cca27a9 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -8,10 +8,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 151.3, + "value": 152.26, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.memgraph": { @@ -22,7 +22,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 655.5, @@ -36,7 +36,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 680.72, @@ -50,10 +50,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 173.86, + "value": 170.68, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars": { @@ -64,12 +64,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 26.49, + "value": 26.27, "workload": "graph-benchmark 100k q1" }, + "graphbench.100k.q1.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 18.35, + "workload": "graph-benchmark 100k q1" + }, + "graphbench.100k.q1.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q1.polars_gpu", + "graphbench.100k.q1.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 9.3, + "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q1.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -81,10 +112,10 @@ "graphbench.100k.q1.polars", "graphbench.100k.q1.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 6.6, + "value": 6.5, "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q2.kuzu": { @@ -95,10 +126,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 279.17, + "value": 279.55, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.memgraph": { @@ -109,7 +140,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 468.94, @@ -123,7 +154,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 632.7, @@ -137,10 +168,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 183.72, + "value": 180.22, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars": { @@ -151,12 +182,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 39.68, + "workload": "graph-benchmark 100k q2" + }, + "graphbench.100k.q2.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 38.84, + "value": 29.02, "workload": "graph-benchmark 100k q2" }, + "graphbench.100k.q2.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q2.polars_gpu", + "graphbench.100k.q2.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 6.2, + "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q2.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -168,10 +230,10 @@ "graphbench.100k.q2.polars", "graphbench.100k.q2.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 4.7, + "value": 4.5, "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q3.kuzu": { @@ -182,10 +244,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 34.16, + "value": 34.71, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.memgraph": { @@ -196,7 +258,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 30.17, @@ -210,7 +272,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 51.14, @@ -224,10 +286,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 76.29, + "value": 75.89, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars": { @@ -238,12 +300,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 9.7, + "workload": "graph-benchmark 100k q3" + }, + "graphbench.100k.q3.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 9.9, + "value": 9.32, "workload": "graph-benchmark 100k q3" }, + "graphbench.100k.q3.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q3.polars_gpu", + "graphbench.100k.q3.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 8.1, + "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q3.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -255,10 +348,10 @@ "graphbench.100k.q3.polars", "graphbench.100k.q3.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 7.7, + "value": 7.8, "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q4.kuzu": { @@ -269,10 +362,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 13.78, + "value": 13.41, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.memgraph": { @@ -283,7 +376,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 37.64, @@ -297,7 +390,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 63.76, @@ -311,10 +404,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 69.28, + "value": 68.95, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars": { @@ -325,12 +418,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 8.97, + "workload": "graph-benchmark 100k q4" + }, + "graphbench.100k.q4.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 10.15, + "value": 9.84, "workload": "graph-benchmark 100k q4" }, + "graphbench.100k.q4.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q4.polars_gpu", + "graphbench.100k.q4.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 7.0, + "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q4.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -342,10 +466,10 @@ "graphbench.100k.q4.polars", "graphbench.100k.q4.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 6.8, + "value": 7.7, "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q5.kuzu": { @@ -356,10 +480,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 12.54, + "value": 13.18, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.memgraph": { @@ -370,7 +494,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 3.95, @@ -384,7 +508,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 8.96, @@ -398,10 +522,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 411.6, + "value": 409.36, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars": { @@ -412,12 +536,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 11.33, + "value": 12.85, "workload": "graph-benchmark 100k q5" }, + "graphbench.100k.q5.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 13.74, + "workload": "graph-benchmark 100k q5" + }, + "graphbench.100k.q5.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q5.polars_gpu", + "graphbench.100k.q5.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 29.8, + "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q5.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -429,10 +584,10 @@ "graphbench.100k.q5.polars", "graphbench.100k.q5.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 36.3, + "value": 31.9, "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q6.kuzu": { @@ -443,10 +598,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 24.42, + "value": 24.14, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.memgraph": { @@ -457,7 +612,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 5.14, @@ -471,7 +626,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 22.94, @@ -485,10 +640,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 410.3, + "value": 416.88, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars": { @@ -499,12 +654,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 12.24, + "value": 12.43, "workload": "graph-benchmark 100k q6" }, + "graphbench.100k.q6.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 14.55, + "workload": "graph-benchmark 100k q6" + }, + "graphbench.100k.q6.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q6.polars_gpu", + "graphbench.100k.q6.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 28.7, + "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q6.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -516,7 +702,7 @@ "graphbench.100k.q6.polars", "graphbench.100k.q6.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", "value": 33.5, @@ -530,10 +716,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 9.57, + "value": 9.25, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.memgraph": { @@ -544,7 +730,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 3.74, @@ -558,7 +744,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 140.18, @@ -572,10 +758,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 120.63, + "value": 124.41, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars": { @@ -586,12 +772,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 6.94, + "value": 7.16, "workload": "graph-benchmark 100k q7" }, + "graphbench.100k.q7.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 14.25, + "workload": "graph-benchmark 100k q7" + }, + "graphbench.100k.q7.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q7.polars_gpu", + "graphbench.100k.q7.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 8.7, + "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q7.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -603,7 +820,7 @@ "graphbench.100k.q7.polars", "graphbench.100k.q7.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", "value": 17.4, @@ -617,10 +834,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 13.45, + "value": 9.87, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.memgraph": { @@ -631,7 +848,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 7316.46, @@ -645,7 +862,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 3766.74, @@ -659,10 +876,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 88.78, + "value": 90.33, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars": { @@ -673,12 +890,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 13.6, + "value": 13.76, "workload": "graph-benchmark 100k q8" }, + "graphbench.100k.q8.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 171.72, + "workload": "graph-benchmark 100k q8" + }, + "graphbench.100k.q8.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q8.polars_gpu", + "graphbench.100k.q8.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 0.5, + "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q8.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -690,10 +938,10 @@ "graphbench.100k.q8.polars", "graphbench.100k.q8.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 6.5, + "value": 6.6, "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q9.kuzu": { @@ -704,10 +952,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 84.32, + "value": 83.13, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.memgraph": { @@ -718,7 +966,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 6199.8, @@ -732,7 +980,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", "value": 4843.46, @@ -746,10 +994,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 162.06, + "value": 167.96, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars": { @@ -760,12 +1008,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "ms", + "value": 34.2, + "workload": "graph-benchmark 100k q9" + }, + "graphbench.100k.q9.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "ms", - "value": 36.33, + "value": 23.71, "workload": "graph-benchmark 100k q9" }, + "graphbench.100k.q9.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q9.polars_gpu", + "graphbench.100k.q9.pandas" + ], + "run": "graphbench-q1q9-100k-20260904", + "status": "ok", + "unit": "x", + "value": 7.1, + "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.100k.q9.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -777,10 +1056,10 @@ "graphbench.100k.q9.polars", "graphbench.100k.q9.pandas" ], - "run": "graphbench-q1q9-100k-20260813", + "run": "graphbench-q1q9-100k-20260904", "status": "ok", "unit": "x", - "value": 4.5, + "value": 4.9, "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q1.kuzu": { @@ -791,10 +1070,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 15.77, + "value": 15.32, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.memgraph": { @@ -805,7 +1084,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 66.08, @@ -819,7 +1098,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 74.5, @@ -833,10 +1112,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 27.6, + "value": 30.06, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars": { @@ -847,12 +1126,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 7.34, + "value": 7.54, "workload": "graph-benchmark 20k q1" }, + "graphbench.20k.q1.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 6.37, + "workload": "graph-benchmark 20k q1" + }, + "graphbench.20k.q1.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q1.polars_gpu", + "graphbench.20k.q1.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 4.7, + "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q1.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -864,10 +1174,10 @@ "graphbench.20k.q1.polars", "graphbench.20k.q1.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 3.8, + "value": 4.0, "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q2.kuzu": { @@ -878,10 +1188,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 41.03, + "value": 36.01, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.memgraph": { @@ -892,7 +1202,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 44.41, @@ -906,7 +1216,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 67.74, @@ -920,10 +1230,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 30.16, + "value": 32.83, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars": { @@ -934,12 +1244,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 11.79, + "workload": "graph-benchmark 20k q2" + }, + "graphbench.20k.q2.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 11.05, + "value": 10.26, "workload": "graph-benchmark 20k q2" }, + "graphbench.20k.q2.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q2.polars_gpu", + "graphbench.20k.q2.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 3.2, + "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q2.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -951,10 +1292,10 @@ "graphbench.20k.q2.polars", "graphbench.20k.q2.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 2.7, + "value": 2.8, "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q3.kuzu": { @@ -965,10 +1306,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 5.68, + "value": 5.86, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.memgraph": { @@ -979,7 +1320,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 4.16, @@ -993,7 +1334,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 8.31, @@ -1007,7 +1348,7 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 11.32, @@ -1021,12 +1362,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 4.79, + "workload": "graph-benchmark 20k q3" + }, + "graphbench.20k.q3.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 4.69, + "value": 4.71, "workload": "graph-benchmark 20k q3" }, + "graphbench.20k.q3.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q3.polars_gpu", + "graphbench.20k.q3.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 2.4, + "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q3.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1038,7 +1410,7 @@ "graphbench.20k.q3.polars", "graphbench.20k.q3.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", "value": 2.4, @@ -1052,10 +1424,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 3.18, + "value": 3.4, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.memgraph": { @@ -1066,7 +1438,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 6.38, @@ -1080,7 +1452,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 11.54, @@ -1094,10 +1466,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 9.84, + "value": 9.88, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars": { @@ -1108,12 +1480,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 3.58, + "workload": "graph-benchmark 20k q4" + }, + "graphbench.20k.q4.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 3.23, + "value": 16.66, "workload": "graph-benchmark 20k q4" }, + "graphbench.20k.q4.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q4.polars_gpu", + "graphbench.20k.q4.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 0.6, + "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q4.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1125,10 +1528,10 @@ "graphbench.20k.q4.polars", "graphbench.20k.q4.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 3.0, + "value": 2.8, "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q5.kuzu": { @@ -1139,10 +1542,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 5.18, + "value": 5.23, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.memgraph": { @@ -1153,7 +1556,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 4.63, @@ -1167,7 +1570,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 18.33, @@ -1181,10 +1584,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 79.44, + "value": 79.54, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars": { @@ -1195,12 +1598,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 4.2, + "value": 4.1, "workload": "graph-benchmark 20k q5" }, + "graphbench.20k.q5.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 7.21, + "workload": "graph-benchmark 20k q5" + }, + "graphbench.20k.q5.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q5.polars_gpu", + "graphbench.20k.q5.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 11.0, + "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q5.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1212,10 +1646,10 @@ "graphbench.20k.q5.polars", "graphbench.20k.q5.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 18.9, + "value": 19.4, "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q6.kuzu": { @@ -1226,10 +1660,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 8.67, + "value": 8.9, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.memgraph": { @@ -1240,7 +1674,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 4.85, @@ -1254,7 +1688,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 20.99, @@ -1268,10 +1702,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 79.42, + "value": 81.02, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars": { @@ -1282,12 +1716,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 4.79, + "value": 4.99, "workload": "graph-benchmark 20k q6" }, + "graphbench.20k.q6.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 8.03, + "workload": "graph-benchmark 20k q6" + }, + "graphbench.20k.q6.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q6.polars_gpu", + "graphbench.20k.q6.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 10.1, + "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q6.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1299,10 +1764,10 @@ "graphbench.20k.q6.polars", "graphbench.20k.q6.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 16.6, + "value": 16.3, "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q7.kuzu": { @@ -1313,10 +1778,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 5.13, + "value": 5.22, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.memgraph": { @@ -1327,7 +1792,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 5.39, @@ -1341,7 +1806,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 15.14, @@ -1355,10 +1820,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 19.52, + "value": 20.03, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars": { @@ -1369,12 +1834,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 3.25, + "value": 3.26, "workload": "graph-benchmark 20k q7" }, + "graphbench.20k.q7.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 8.35, + "workload": "graph-benchmark 20k q7" + }, + "graphbench.20k.q7.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q7.polars_gpu", + "graphbench.20k.q7.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 2.4, + "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q7.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1386,10 +1882,10 @@ "graphbench.20k.q7.polars", "graphbench.20k.q7.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 6.0, + "value": 6.2, "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q8.kuzu": { @@ -1400,10 +1896,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 2.69, + "value": 2.77, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.memgraph": { @@ -1414,7 +1910,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 198.8, @@ -1428,7 +1924,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 198.71, @@ -1442,10 +1938,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 11.5, + "value": 12.5, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars": { @@ -1456,12 +1952,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 2.57, + "workload": "graph-benchmark 20k q8" + }, + "graphbench.20k.q8.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 2.08, + "value": 2.6, "workload": "graph-benchmark 20k q8" }, + "graphbench.20k.q8.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q8.polars_gpu", + "graphbench.20k.q8.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 4.8, + "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q8.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1473,10 +2000,10 @@ "graphbench.20k.q8.polars", "graphbench.20k.q8.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 5.5, + "value": 4.9, "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q9.kuzu": { @@ -1487,10 +2014,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 10.68, + "value": 10.98, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.memgraph": { @@ -1501,7 +2028,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 124.91, @@ -1515,7 +2042,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", "value": 422.29, @@ -1529,10 +2056,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 22.35, + "value": 22.32, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars": { @@ -1543,12 +2070,43 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "ms", - "value": 8.81, + "value": 7.79, "workload": "graph-benchmark 20k q9" }, + "graphbench.20k.q9.polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "ms", + "value": 6.89, + "workload": "graph-benchmark 20k q9" + }, + "graphbench.20k.q9.polars_gpu_vs_pandas": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [], + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.20k.q9.polars_gpu", + "graphbench.20k.q9.pandas" + ], + "run": "graphbench-q1q9-20k-20260904", + "status": "ok", + "unit": "x", + "value": 3.2, + "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, "graphbench.20k.q9.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, @@ -1560,10 +2118,10 @@ "graphbench.20k.q9.polars", "graphbench.20k.q9.pandas" ], - "run": "graphbench-q1q9-20k-20260813", + "run": "graphbench-q1q9-20k-20260904", "status": "ok", "unit": "x", - "value": 2.5, + "value": 2.9, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" }, "pagerank.gplus.gfql_cpu": { @@ -2234,12 +2792,10 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T06:03:03Z", - "generated_by": "graphistry/pyg-bench 48a65e2d scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T06:59:50Z", + "generated_by": "graphistry/pyg-bench bf2c7e30 scripts/export_docs_numbers.py", "policy": { "drift_waivers": { - "graphbench-q1q9-100k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", - "graphbench-q1q9-20k-20260813": "Relaned board measured at release-candidate pygraphistry 4d301b18 (274 graphistry/compute commits before 0.59.0). A release-commit re-measurement with a polars-gpu column is tracked; publish this board with its provenance until then.", "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." }, "literal_allowlist": {}, @@ -2292,35 +2848,35 @@ "row_validation": "All 12 selected-node sets had Jaccard 1.0 against the reference (gate 0.95); every slot passed typed result, exact run-contract, and load/self-spike validation; the committed aggregate exactly recomputes from all slots.", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13); GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-100k-20260813": { - "artifact": "results/graphbench-board-relane-20260813/100k with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-100k-20260904": { + "artifact": "results/graphbench-059-gpu-100k-20260904 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 100,000 persons / 2,417,738 FOLLOWS / 2,775,195 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-08-13", + "measured_at": "2026-09-04", "perf_lock_held": true, - "pyg_bench_commit": "8ee0dee9", - "pygraphistry_commit": "4d301b18", + "pyg_bench_commit": "d028118", + "pygraphistry_commit": "3fb216dd", "quiet_host": true, - "reps": "GFQL and Kuzu: 3 warmups then timed runs per slot, 4 position-balanced slots per engine (G/K), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included). Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", - "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-20k-20260813": { - "artifact": "results/graphbench-board-relane-20260813/20k with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-20k-20260904": { + "artifact": "results/graphbench-059-gpu-20k-20260904 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 20,000 persons / 199,939 FOLLOWS / 260,107 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-08-12", + "measured_at": "2026-09-04", "perf_lock_held": true, - "pyg_bench_commit": "8ee0dee9", - "pygraphistry_commit": "4d301b18", + "pyg_bench_commit": "d028118", + "pygraphistry_commit": "3fb216dd", "quiet_host": true, - "reps": "GFQL and Kuzu: 3 warmups then timed runs per slot, 4 position-balanced slots per engine (G/K), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included). Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", - "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 384e898699..febd49e440 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -14,8 +14,8 @@ GFQL runs the same query on ``pandas`` (the default), ``polars`` (CPU), ``cudf`` (NVIDIA GPU), or ``polars-gpu``. Each engine returns the same rows, or GFQL reports an error before execution instead of changing engines. On the q1–q9 boards below, the Polars engine is faster than pandas on :bench-tally:`graphbench.100k|polars|pandas` -queries at 100,000 people, by up to :bench:`graphbench.100k.q5.polars_vs_pandas` -(q5). See :doc:`engines` for the selection guide. +queries at 100,000 people, by up to :bench:`graphbench.100k.q6.polars_vs_pandas` +(q6). See :doc:`engines` for the selection guide. .. doc-test: skip @@ -34,9 +34,14 @@ filter records, and count two-hop paths on synthetic social graphs with 20,000 a 100,000 people. Every cell passed result-row validation against every other engine. Times are milliseconds; lower is better. -GFQL binds the graph cold inside every timed run. Kuzu compiles the query text on each -call. Memgraph and Neo4j answer over Bolt with their default plan caches. These are -direct times under those profiles, not cross-engine speedup ratios. +GFQL binds the graph cold inside every timed run; the ``polars-gpu`` column runs the +same fused plan on the GPU. Kuzu compiles the query text on each call. Memgraph and +Neo4j answer over Bolt with their default plan caches. These are direct times under +those profiles, not cross-engine speedup ratios. At these sizes the queries are +millisecond-scale, so the GPU engine wins some and loses others to the CPU engine: +:bench-tally:`graphbench.100k|polars_gpu|polars` at 100,000 people. Its widest loss is +q8 at 100,000 people, :bench:`graphbench.100k.q8.polars_gpu` against +:bench:`graphbench.100k.q8.polars` on the CPU. At 20,000 people, GFQL Polars is faster than Kuzu on :bench-tally:`graphbench.20k|polars|kuzu` queries, than Memgraph on @@ -44,16 +49,18 @@ At 20,000 people, GFQL Polars is faster than Kuzu on :bench-tally:`graphbench.20k|polars|neo4j`. At 100,000 people the counts are :bench-tally:`graphbench.100k|polars|kuzu` (Kuzu), :bench-tally:`graphbench.100k|polars|memgraph` (Memgraph), and -:bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Memgraph wins q5, q6, and q7 at -100,000 people: its planner starts from the ten-node interest side, which GFQL's -Cypher path does not yet do. +:bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Kuzu wins q4 at 20,000 people and +q8 at 100,000 people; the artifact's compare tables classify both as ties because the +per-slot medians overlap. Memgraph wins q3 and q6 at 20,000 people and q5, q6, and q7 at +100,000 people, where Neo4j also wins q5: their planners start from the ten-node +interest side, which GFQL's Cypher path does not yet do. The 20,000-person board ~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 - :widths: 8 10 14 14 14 20 20 + :widths: 8 8 12 12 12 16 16 16 * - Query - Rows @@ -62,6 +69,7 @@ The 20,000-person board - Neo4j - GFQL ``pandas`` - GFQL ``polars`` + - GFQL ``polars-gpu`` * - q1 - 3 - :bench:`graphbench.20k.q1.kuzu` @@ -69,6 +77,7 @@ The 20,000-person board - :bench:`graphbench.20k.q1.neo4j` - :bench:`graphbench.20k.q1.pandas` - :bench:`graphbench.20k.q1.polars` + - :bench:`graphbench.20k.q1.polars_gpu` * - q2 - 1 - :bench:`graphbench.20k.q2.kuzu` @@ -76,6 +85,7 @@ The 20,000-person board - :bench:`graphbench.20k.q2.neo4j` - :bench:`graphbench.20k.q2.pandas` - :bench:`graphbench.20k.q2.polars` + - :bench:`graphbench.20k.q2.polars_gpu` * - q3 - 5 - :bench:`graphbench.20k.q3.kuzu` @@ -83,6 +93,7 @@ The 20,000-person board - :bench:`graphbench.20k.q3.neo4j` - :bench:`graphbench.20k.q3.pandas` - :bench:`graphbench.20k.q3.polars` + - :bench:`graphbench.20k.q3.polars_gpu` * - q4 - 2 - :bench:`graphbench.20k.q4.kuzu` @@ -90,6 +101,7 @@ The 20,000-person board - :bench:`graphbench.20k.q4.neo4j` - :bench:`graphbench.20k.q4.pandas` - :bench:`graphbench.20k.q4.polars` + - :bench:`graphbench.20k.q4.polars_gpu` * - q5 - 1 - :bench:`graphbench.20k.q5.kuzu` @@ -97,6 +109,7 @@ The 20,000-person board - :bench:`graphbench.20k.q5.neo4j` - :bench:`graphbench.20k.q5.pandas` - :bench:`graphbench.20k.q5.polars` + - :bench:`graphbench.20k.q5.polars_gpu` * - q6 - 5 - :bench:`graphbench.20k.q6.kuzu` @@ -104,6 +117,7 @@ The 20,000-person board - :bench:`graphbench.20k.q6.neo4j` - :bench:`graphbench.20k.q6.pandas` - :bench:`graphbench.20k.q6.polars` + - :bench:`graphbench.20k.q6.polars_gpu` * - q7 - 1 - :bench:`graphbench.20k.q7.kuzu` @@ -111,6 +125,7 @@ The 20,000-person board - :bench:`graphbench.20k.q7.neo4j` - :bench:`graphbench.20k.q7.pandas` - :bench:`graphbench.20k.q7.polars` + - :bench:`graphbench.20k.q7.polars_gpu` * - q8 - 1 - :bench:`graphbench.20k.q8.kuzu` @@ -118,6 +133,7 @@ The 20,000-person board - :bench:`graphbench.20k.q8.neo4j` - :bench:`graphbench.20k.q8.pandas` - :bench:`graphbench.20k.q8.polars` + - :bench:`graphbench.20k.q8.polars_gpu` * - q9 - 1 - :bench:`graphbench.20k.q9.kuzu` @@ -125,13 +141,14 @@ The 20,000-person board - :bench:`graphbench.20k.q9.neo4j` - :bench:`graphbench.20k.q9.pandas` - :bench:`graphbench.20k.q9.polars` + - :bench:`graphbench.20k.q9.polars_gpu` The 100,000-person board ~~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 - :widths: 8 10 14 14 14 20 20 + :widths: 8 8 12 12 12 16 16 16 * - Query - Rows @@ -140,6 +157,7 @@ The 100,000-person board - Neo4j - GFQL ``pandas`` - GFQL ``polars`` + - GFQL ``polars-gpu`` * - q1 - 3 - :bench:`graphbench.100k.q1.kuzu` @@ -147,6 +165,7 @@ The 100,000-person board - :bench:`graphbench.100k.q1.neo4j` - :bench:`graphbench.100k.q1.pandas` - :bench:`graphbench.100k.q1.polars` + - :bench:`graphbench.100k.q1.polars_gpu` * - q2 - 1 - :bench:`graphbench.100k.q2.kuzu` @@ -154,6 +173,7 @@ The 100,000-person board - :bench:`graphbench.100k.q2.neo4j` - :bench:`graphbench.100k.q2.pandas` - :bench:`graphbench.100k.q2.polars` + - :bench:`graphbench.100k.q2.polars_gpu` * - q3 - 5 - :bench:`graphbench.100k.q3.kuzu` @@ -161,6 +181,7 @@ The 100,000-person board - :bench:`graphbench.100k.q3.neo4j` - :bench:`graphbench.100k.q3.pandas` - :bench:`graphbench.100k.q3.polars` + - :bench:`graphbench.100k.q3.polars_gpu` * - q4 - 3 - :bench:`graphbench.100k.q4.kuzu` @@ -168,6 +189,7 @@ The 100,000-person board - :bench:`graphbench.100k.q4.neo4j` - :bench:`graphbench.100k.q4.pandas` - :bench:`graphbench.100k.q4.polars` + - :bench:`graphbench.100k.q4.polars_gpu` * - q5 - 1 - :bench:`graphbench.100k.q5.kuzu` @@ -175,6 +197,7 @@ The 100,000-person board - :bench:`graphbench.100k.q5.neo4j` - :bench:`graphbench.100k.q5.pandas` - :bench:`graphbench.100k.q5.polars` + - :bench:`graphbench.100k.q5.polars_gpu` * - q6 - 5 - :bench:`graphbench.100k.q6.kuzu` @@ -182,6 +205,7 @@ The 100,000-person board - :bench:`graphbench.100k.q6.neo4j` - :bench:`graphbench.100k.q6.pandas` - :bench:`graphbench.100k.q6.polars` + - :bench:`graphbench.100k.q6.polars_gpu` * - q7 - 1 - :bench:`graphbench.100k.q7.kuzu` @@ -189,6 +213,7 @@ The 100,000-person board - :bench:`graphbench.100k.q7.neo4j` - :bench:`graphbench.100k.q7.pandas` - :bench:`graphbench.100k.q7.polars` + - :bench:`graphbench.100k.q7.polars_gpu` * - q8 - 1 - :bench:`graphbench.100k.q8.kuzu` @@ -196,6 +221,7 @@ The 100,000-person board - :bench:`graphbench.100k.q8.neo4j` - :bench:`graphbench.100k.q8.pandas` - :bench:`graphbench.100k.q8.polars` + - :bench:`graphbench.100k.q8.polars_gpu` * - q9 - 1 - :bench:`graphbench.100k.q9.kuzu` @@ -203,6 +229,7 @@ The 100,000-person board - :bench:`graphbench.100k.q9.neo4j` - :bench:`graphbench.100k.q9.pandas` - :bench:`graphbench.100k.q9.polars` + - :bench:`graphbench.100k.q9.polars_gpu` .. _gfql-snb-aligned: @@ -338,7 +365,7 @@ Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.js which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` reject missing, stale, or unpublished values. -.. bench-provenance:: graphbench-q1q9-20k-20260813 graphbench-q1q9-100k-20260813 snb-aligned-release-20260902 +.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 :disclosures: Next steps From 0435e04c4fdeebdd278af6b7459e6f69de166c83 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 09:03:03 -0700 Subject: [PATCH 09/29] docs(gfql): GraphFrames page from published ladder cells; charts shade the PageRank solver Vendor pyg-bench's GraphFrames ladder publication (LiveJournal and Orkut: host-Spark GraphFrames baselines, GFQL GPU PageRank with the solver stage as a component cell, and the released code's LJ filter/hop rows as diagnostic cells behind #2023). The page now prints cells only, with one multi-run provenance block; the June saved results and the stale parity file are gone. The chart generator reads the ladder cells (no results.json), draws the solver share as the solid part of each GFQL PageRank bar and the rest of the query as the light part, marks unmeasured systems, and is tested on a synthetic ladder. Friendster is named as the next rung with the reason it waits on #2024. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 368 +++++++++++++++++- docs/source/_ext/gfql_bench_charts.py | 181 ++++++--- .../bench_graphframes_pagerank_parity.json | 15 - .../_static/graphframes/livejournal_tasks.svg | 40 +- .../gfql/_static/graphframes/orkut_tasks.svg | 40 +- .../gfql/_static/graphframes/results.json | 198 ---------- docs/source/gfql/benchmark_graphframes.rst | 284 +++++++------- docs/test_bench_numbers.py | 66 ++++ 8 files changed, 723 insertions(+), 469 deletions(-) delete mode 100644 docs/source/gfql/_static/graphframes/bench_graphframes_pagerank_parity.json delete mode 100644 docs/source/gfql/_static/graphframes/results.json diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index f69cca27a9..5d5b96fd16 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -2124,6 +2124,340 @@ "value": 2.9, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" }, + "graphframes.lj.filter.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 80.4, + "workload": "GraphFrames tasks LiveJournal filter" + }, + "graphframes.lj.hop1.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 1460.2, + "workload": "GraphFrames tasks LiveJournal hop1" + }, + "graphframes.lj.hop2.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 3475.8, + "workload": "GraphFrames tasks LiveJournal hop2" + }, + "graphframes.lj.pagerank.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 890.9, + "workload": "GraphFrames tasks LiveJournal pagerank" + }, + "graphframes.lj.pagerank.gfql_polars_gpu_kernel": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Component of graphframes.lj.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 59.3, + "workload": "GraphFrames tasks LiveJournal pagerank \u2014 gfql-polars-gpu kernel only" + }, + "graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.pagerank.gfql_polars_gpu", + "graphframes.lj.pagerank.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 18.3, + "workload": "GraphFrames tasks LiveJournal pagerank \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.lj.pagerank.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 16311.2, + "workload": "GraphFrames tasks LiveJournal pagerank" + }, + "graphframes.orkut.filter.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 66.1, + "workload": "GraphFrames tasks Orkut filter" + }, + "graphframes.orkut.hop1.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 3632.2, + "workload": "GraphFrames tasks Orkut hop1" + }, + "graphframes.orkut.hop2.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 10885.8, + "workload": "GraphFrames tasks Orkut hop2" + }, + "graphframes.orkut.pagerank.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 2795.7, + "workload": "GraphFrames tasks Orkut pagerank" + }, + "graphframes.orkut.pagerank.gfql_polars_gpu_kernel": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Component of graphframes.orkut.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 151.4, + "workload": "GraphFrames tasks Orkut pagerank \u2014 gfql-polars-gpu kernel only" + }, + "graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.pagerank.gfql_polars_gpu", + "graphframes.orkut.pagerank.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 12.5, + "workload": "GraphFrames tasks Orkut pagerank \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.orkut.pagerank.graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + ], + "engine": "graphframes", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 34860.7, + "workload": "GraphFrames tasks Orkut pagerank" + }, + "graphframes_059.lj.filter.gfql_polars": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 27.0, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal filter" + }, + "graphframes_059.lj.filter.gfql_polars_gpu": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 27.9, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal filter" + }, + "graphframes_059.lj.hop1.gfql_polars": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 222.2, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal hop1" + }, + "graphframes_059.lj.hop1.gfql_polars_gpu": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 1174.4, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal hop1" + }, + "graphframes_059.lj.hop2.gfql_polars": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 64273.1, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal hop2" + }, + "graphframes_059.lj.hop2.gfql_polars_gpu": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "GFQL 0.59.0 (3fb216dd) hop rows on LiveJournal, kept as the measured before-state of issue #2023; the published GraphFrames comparison uses the #2024 rows." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-059-hops-20260904", + "status": "ok", + "unit": "ms", + "value": 63202.1, + "workload": "GraphFrames tasks at 0.59.0 (diagnostic) LiveJournal hop2" + }, "pagerank.gplus.gfql_cpu": { "board_quotable": true, "comparison_allowed": true, @@ -2792,8 +3126,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T06:59:50Z", - "generated_by": "graphistry/pyg-bench bf2c7e30 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T15:58:17Z", + "generated_by": "graphistry/pyg-bench 5a1a1663 scripts/export_docs_numbers.py", "policy": { "drift_waivers": { "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." @@ -2878,6 +3212,36 @@ "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, + "graphframes-ladder-059-hops-20260904": { + "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", + "competitor": "Apache Spark GraphFrames", + "competitor_version": "graphframes assembly jar 1a4826bc (sha256 in each rung's SOURCE_COMMITS) on PySpark 3.5.3, OpenJDK 1.8.0_492, Spark local[*]", + "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / dataset 90th percentile", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-04", + "perf_lock_held": true, + "pyg_bench_commit": "0f8e213", + "pygraphistry_commit": "3fb216dd", + "quiet_host": true, + "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", + "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + }, + "graphframes-ladder-20260904": { + "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", + "competitor": "Apache Spark GraphFrames", + "competitor_version": "graphframes assembly jar 1a4826bc (sha256 in each rung's SOURCE_COMMITS) on PySpark 3.5.3, OpenJDK 1.8.0_492, Spark local[*]", + "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / dataset 90th percentile", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-04", + "perf_lock_held": true, + "pyg_bench_commit": "0f8e213", + "pygraphistry_commit": "840a780", + "quiet_host": true, + "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", + "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", "competitor": "Kuzu, Neo4j, Memgraph", diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index 95d3448b7d..cc35f74374 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -7,8 +7,8 @@ committed file differs, so a chart cannot go on asserting a number the artifact no longer publishes - which is exactly how withdrawn figures survived on this page as glyph paths. -The GraphFrames task charts render the same way from -``docs/source/gfql/_static/graphframes/results.json``. +The GraphFrames task charts render the same way from the ``graphframes.*`` cells of the +same artifact, once pyg-bench publishes the ladder. Regenerate after vendoring a new artifact:: @@ -35,11 +35,10 @@ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'gfql', '_static', 'filter_pagerank') -#: The GraphFrames comparison renders from its own saved results, next to its charts. +#: The GraphFrames task charts live next to their page's other static assets. GRAPHFRAMES_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'gfql', '_static', 'graphframes') -GRAPHFRAMES_RESULTS_JSON = os.path.join(GRAPHFRAMES_DIR, 'results.json') WIDTH = 720 PAD = 16 @@ -214,12 +213,16 @@ def render(name: str, payload: JSONObject) -> str: #: Bars are scaled per task (the slowest system fills the row) because task times span #: five orders of magnitude; every bar prints its own value, and every GFQL bar prints #: its own ratio against GraphFrames, so no visual comparison is made across tasks. +#: Every figure is a published cell of ``graphframes...``; a +#: ``..._kernel`` component cell, when published, shades the PageRank solver's share of +#: the bar in the full tone and the rest of the query in the light tone. GF_LABEL_W = 178 GF_BAR_MAX = 300 GF_GROUP_HEAD = 24 GF_BAR_ROW = 28 GF_BAR_H = 18 GF_GROUP_GAP = 8 +GF_PREFIX = 'graphframes' GF_TASKS = ( ('filter', 'filter: degree >= {threshold}'), ('hop1', '1-hop from 50 seeds'), @@ -227,78 +230,103 @@ def render(name: str, payload: JSONObject) -> str: ('pagerank', 'PageRank, full graph'), ) GF_SYSTEMS = ( - ('gfql-polars', 'GFQL polars (CPU)', 'cpu'), - ('gfql-polars-gpu', 'GFQL polars-gpu (GPU)', 'gpu'), + ('gfql_polars', 'GFQL polars (CPU)', 'cpu'), + ('gfql_polars_gpu', 'GFQL polars-gpu (GPU)', 'gpu'), ('graphframes', 'GraphFrames local[*]', 'neo'), ) -GF_CHARTS: dict[str, tuple[str, str, int]] = OrderedDict(( - ('livejournal_tasks.svg', ('lj', 'LiveJournal', 42)), - ('orkut_tasks.svg', ('orkut', 'Orkut', 162)), +GF_BASELINE = 'graphframes' + + +class GFChart(NamedTuple): + dataset: str #: key segment in the published cells + title: str #: dataset label with its cardinalities (dataset facts, not timings) + threshold: int #: the filter task's degree threshold, printed in the task label + + +GF_CHARTS: dict[str, GFChart] = OrderedDict(( + ('livejournal_tasks.svg', GFChart('lj', 'LiveJournal: 3,997,962 nodes / 34,681,189 edges', 42)), + ('orkut_tasks.svg', GFChart('orkut', 'Orkut: 3,072,441 nodes / 117,185,083 edges', 162)), )) -def _gf_time(task: str, ms: float) -> str: - if task == 'pagerank': - seconds = ms / 1000.0 - return '{:.2f}s'.format(seconds) if seconds < 10 else '{:.1f}s'.format(seconds) - return '{:.1f}ms'.format(ms) +def gf_cell_key(dataset: str, task: str, system: str) -> str: + return '{}.{}.{}.{}'.format(GF_PREFIX, dataset, task, system) -def _gf_ratio(system_ms: float, graphframes_ms: float) -> str: - ratio = graphframes_ms / system_ms - if ratio >= 1: - return '{:.1f}x faster'.format(ratio) - return '{:.2f}x (slower)'.format(ratio) +def gf_published(payload: JSONObject) -> bool: + """Whether the artifact publishes the GraphFrames ladder at all.""" + cells = payload.get('cells') + return isinstance(cells, dict) and any(k.startswith(GF_PREFIX + '.') for k in cells) -def _gf_dataset(results: JSONObject, key: str) -> JSONObject: - dataset = results.get(key) - if not isinstance(dataset, dict): - raise ChartError('{} has no dataset {!r}'.format( - os.path.basename(GRAPHFRAMES_RESULTS_JSON), key)) - return dataset +def gf_cell_keys(payload: JSONObject) -> list[str]: + """Every published cell the GraphFrames charts draw.""" + cells = payload.get('cells') + if not isinstance(cells, dict): + return [] + keys: list[str] = [] + for chart in GF_CHARTS.values(): + for task, _ in GF_TASKS: + for system, _, _ in GF_SYSTEMS: + key = gf_cell_key(chart.dataset, task, system) + for candidate in (key, key + '_kernel', key + '_vs_' + GF_BASELINE): + if candidate in cells: + keys.append(candidate) + return keys + + +def _gf_optional(payload: JSONObject, key: str) -> JSONObject | None: + cells = payload.get('cells') + cell = cells.get(key) if isinstance(cells, dict) else None + return cell if isinstance(cell, dict) else None -def _gf_median_ms(dataset: JSONObject, task: str, system: str) -> float: - tasks = dataset.get('tasks') - arms = tasks.get(task) if isinstance(tasks, dict) else None - arm = arms.get(system) if isinstance(arms, dict) else None - value = arm.get('median_ms') if isinstance(arm, dict) else None - if isinstance(value, bool) or not isinstance(value, (int, float)): - raise ChartError('{}.{}.{} has no median_ms'.format( - dataset.get('name', '?'), task, system)) +def _gf_ms(cell: JSONObject, key: str) -> float: + value = cell.get('value') + if isinstance(value, bool) or not isinstance(value, (int, float)) or cell.get('unit') != 'ms': + raise ChartError('{!r} is not a millisecond figure'.format(key)) return float(value) -def render_graphframes(name: str, results: JSONObject) -> str: - """Render one GraphFrames task chart to SVG text.""" - key, label, threshold = GF_CHARTS[name] - dataset = _gf_dataset(results, key) - edges = dataset.get('n_edges') - nodes_ = dataset.get('n_nodes') - title = '{}: {:,} nodes / {:,} edges'.format(label, int(nodes_), int(edges)) - subtitle = ('Median of 5 after 2 warmups; same result size on every system. ' - 'Lower is better.') +def _gf_ratio_text(ratio: JSONObject) -> str: + text = format_cell(ratio) + value = ratio['value'] + assert isinstance(value, (int, float)) + return '{} faster than GraphFrames'.format(text) if value >= 1 else \ + '{} of GraphFrames’ speed (slower)'.format(text) + + +def render_graphframes(name: str, payload: JSONObject) -> str: + """Render one GraphFrames task chart from the published ladder cells.""" + chart = GF_CHARTS[name] + subtitle = ('Median of 5 after 2 warmups; same result size on every system unless ' + 'marked. Lower is better.') group_h = GF_GROUP_HEAD + GF_BAR_ROW * len(GF_SYSTEMS) + GF_GROUP_GAP height = HEADER_H + group_h * len(GF_TASKS) + FOOT_H out = [ ''.format(WIDTH, height, WIDTH, height, _esc(title)), - '{}'.format(_esc(title)), + ' role="img" aria-label="{}">'.format(WIDTH, height, WIDTH, height, _esc(chart.title)), + '{}'.format(_esc(chart.title)), ''.format(STYLE), '', ''.format(WIDTH, height), - '{}'.format(PAD, _esc(title)), + '{}'.format(PAD, _esc(chart.title)), '{}'.format(PAD, _esc(subtitle)), ] bar_x = PAD + GF_LABEL_W + drawn = 0 + shaded = False for group_index, (task, task_label) in enumerate(GF_TASKS): group_top = HEADER_H + group_index * group_h out.append('{}'.format( - PAD, group_top + 15, _esc(task_label.format(threshold=threshold)))) - times = {system: _gf_median_ms(dataset, task, system) for system, _, _ in GF_SYSTEMS} - slowest = max(times.values()) - graphframes_ms = times['graphframes'] + PAD, group_top + 15, _esc(task_label.format(threshold=chart.threshold)))) + rows: dict[str, tuple[JSONObject, float]] = {} + for system, _, _ in GF_SYSTEMS: + key = gf_cell_key(chart.dataset, task, system) + cell = _gf_optional(payload, key) + if cell is not None: + rows[system] = (cell, _gf_ms(cell, key)) + slowest = max((ms for _, ms in rows.values()), default=0.0) out.append(''.format( bar_x - 1, group_top + GF_GROUP_HEAD - 2, GF_BAR_ROW * len(GF_SYSTEMS))) for row_index, (system, system_label, tone) in enumerate(GF_SYSTEMS): @@ -306,19 +334,48 @@ def render_graphframes(name: str, results: JSONObject) -> str: bar_top = row_top + (GF_BAR_ROW - GF_BAR_H) / 2 out.append('{}'.format( PAD + 10, row_top + 18, _esc(system_label))) - width = max(MIN_BAR, GF_BAR_MAX * times[system] / slowest) - out.append(''.format( - tone, _bar_path(bar_x, bar_top, width, GF_BAR_H))) - spans = ['{}'.format(_esc(_gf_time(task, times[system])))] - if system != 'graphframes': + if system not in rows: + out.append('not measured'.format( + _num(bar_x + 8), row_top + 18)) + continue + cell, ms = rows[system] + key = gf_cell_key(chart.dataset, task, system) + width = max(MIN_BAR, GF_BAR_MAX * ms / slowest) + kernel = _gf_optional(payload, key + '_kernel') + if kernel is not None: + kernel_ms = _gf_ms(kernel, key + '_kernel') + if kernel_ms > ms: + raise ChartError('{!r} exceeds its query time'.format(key + '_kernel')) + out.append(''.format( + tone, _bar_path(bar_x, bar_top, width, GF_BAR_H))) + out.append(''.format( + tone, _num(bar_x), _num(bar_top), + _num(max(MIN_BAR, GF_BAR_MAX * kernel_ms / slowest)), GF_BAR_H)) + shaded = True + else: + out.append(''.format( + tone, _bar_path(bar_x, bar_top, width, GF_BAR_H))) + drawn += 1 + spans = ['{}'.format(_esc(format_cell(cell)))] + if kernel is not None: + spans.append('{}'.format( + _esc('(solver {})'.format(format_cell(kernel))))) + ratio = _gf_optional(payload, key + '_vs_' + GF_BASELINE) + if ratio is not None: spans.append('{}'.format( - _esc(_gf_ratio(times[system], graphframes_ms)))) + _esc(_gf_ratio_text(ratio)))) + elif cell.get('status') != 'ok' or not cell.get('comparison_allowed'): + spans.append('diagnostic: result size differs') out.append('{}'.format( _num(bar_x + width + 8), row_top + 18, ''.join(spans))) - out.append('{}'.format( - PAD, height - 11, - _esc('Bars are scaled per task. dgx-spark, single node; ' - 'Spark local[*] over all cores.'))) + if drawn == 0: + raise ChartError('{} draws no published cell for dataset {!r}'.format( + name, chart.dataset)) + foot = 'Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores.' + if shaded: + foot = ('Bars are scaled per task; on PageRank the solid part is the solver alone, ' + 'the light part the rest of the query. Single node; Spark local[*].') + out.append('{}'.format(PAD, height - 11, _esc(foot))) out.append('') return '\n'.join(out) + '\n' @@ -334,9 +391,9 @@ def rendered(payload: JSONObject | None = None) -> dict[str, str]: """Every chart, keyed by file name, rendered from the vendored artifact.""" data = payload if payload is not None else load(BENCHMARKS_JSON) charts = OrderedDict((name, render(name, data)) for name in CHARTS) - results = load(GRAPHFRAMES_RESULTS_JSON) - for name in GF_CHARTS: - charts[name] = render_graphframes(name, results) + if gf_published(data): + for name in GF_CHARTS: + charts[name] = render_graphframes(name, data) return charts diff --git a/docs/source/gfql/_static/graphframes/bench_graphframes_pagerank_parity.json b/docs/source/gfql/_static/graphframes/bench_graphframes_pagerank_parity.json deleted file mode 100644 index a04c670327..0000000000 --- a/docs/source/gfql/_static/graphframes/bench_graphframes_pagerank_parity.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "n_common_vertices": 3997962, - "spearman": { - "igraph_vs_cugraph": 1.0, - "igraph_vs_graphframes": 1.0, - "cugraph_vs_graphframes": 1.0 - }, - "top100_overlap": { - "igraph_vs_cugraph": 100, - "igraph_vs_graphframes": 100, - "cugraph_vs_graphframes": 100 - }, - "dataset": "lj", - "note": "PageRank score agreement across engines; GraphFrames maxIter=20, igraph eps=1e-3, cugraph tol=1e-5" -} \ No newline at end of file diff --git a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg index c158854a23..5a9dec7530 100644 --- a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -4,50 +4,44 @@ LiveJournal: 3,997,962 nodes / 34,681,189 edges -Median of 5 after 2 warmups; same result size on every system. Lower is better. +Median of 5 after 2 warmups; same result size on every system unless marked. Lower is better. filter: degree >= 42 GFQL polars (CPU) - -2.1ms43.0x faster +not measured GFQL polars-gpu (GPU) - -2.4ms37.7x faster +not measured GraphFrames local[*] -90.4ms +80.4 ms 1-hop from 50 seeds GFQL polars (CPU) - -236.8ms6.0x faster +not measured GFQL polars-gpu (GPU) - -191.4ms7.4x faster +not measured GraphFrames local[*] -1421.7ms +1460.2 ms 2-hop from 50 seeds GFQL polars (CPU) - -1669.3ms2.1x faster +not measured GFQL polars-gpu (GPU) - -1542.1ms2.3x faster +not measured GraphFrames local[*] -3583.3ms +3475.8 ms PageRank, full graph GFQL polars (CPU) - -49.3s0.33x (slower) +not measured GFQL polars-gpu (GPU) - -1.11s14.7x faster + + +890.9 ms(solver 59.3 ms)18.3x faster than GraphFrames GraphFrames local[*] - -16.3s -Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores. + +16311.2 ms +Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. diff --git a/docs/source/gfql/_static/graphframes/orkut_tasks.svg b/docs/source/gfql/_static/graphframes/orkut_tasks.svg index a89e5b5a81..cf6e4f3b09 100644 --- a/docs/source/gfql/_static/graphframes/orkut_tasks.svg +++ b/docs/source/gfql/_static/graphframes/orkut_tasks.svg @@ -4,50 +4,44 @@ Orkut: 3,072,441 nodes / 117,185,083 edges -Median of 5 after 2 warmups; same result size on every system. Lower is better. +Median of 5 after 2 warmups; same result size on every system unless marked. Lower is better. filter: degree >= 162 GFQL polars (CPU) - -1.7ms41.5x faster +not measured GFQL polars-gpu (GPU) - -2.0ms35.3x faster +not measured GraphFrames local[*] -70.6ms +66.1 ms 1-hop from 50 seeds GFQL polars (CPU) - -562.9ms6.8x faster +not measured GFQL polars-gpu (GPU) - -442.0ms8.7x faster +not measured GraphFrames local[*] -3826.6ms +3632.2 ms 2-hop from 50 seeds GFQL polars (CPU) - -9439.8ms1.2x faster +not measured GFQL polars-gpu (GPU) - -8860.2ms1.3x faster +not measured GraphFrames local[*] -11582.9ms +10885.8 ms PageRank, full graph GFQL polars (CPU) - -160.1s0.23x (slower) +not measured GFQL polars-gpu (GPU) - -3.50s10.5x faster + + +2795.7 ms(solver 151.4 ms)12.5x faster than GraphFrames GraphFrames local[*] - -36.8s -Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores. + +34860.7 ms +Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. diff --git a/docs/source/gfql/_static/graphframes/results.json b/docs/source/gfql/_static/graphframes/results.json deleted file mode 100644 index e794ad8f2c..0000000000 --- a/docs/source/gfql/_static/graphframes/results.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "lj": { - "n_edges": 34681189, - "n_nodes": 3997962, - "tasks": { - "filter": { - "gfql-polars": { - "cold_load_ms": 2362.8, - "iters": 5, - "median_ms": 2.1, - "result_size": 403561, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 2290.0, - "iters": 5, - "median_ms": 2.4, - "result_size": 403561, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 10255.6, - "iters": 5, - "median_ms": 90.4, - "result_size": 403561, - "warmups": 2 - } - }, - "hop1": { - "gfql-polars": { - "cold_load_ms": 2362.8, - "iters": 5, - "median_ms": 236.8, - "result_size": 119877, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 2290.0, - "iters": 5, - "median_ms": 191.4, - "result_size": 119877, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 10255.6, - "iters": 5, - "median_ms": 1421.7, - "result_size": 119877, - "warmups": 2 - } - }, - "hop2": { - "gfql-polars": { - "cold_load_ms": 2362.8, - "iters": 5, - "median_ms": 1669.3, - "result_size": 1378430, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 2290.0, - "iters": 5, - "median_ms": 1542.1, - "result_size": 1378430, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 10255.6, - "iters": 5, - "median_ms": 3583.3, - "result_size": 1378430, - "warmups": 2 - } - }, - "pagerank": { - "gfql-polars": { - "cold_load_ms": 2362.8, - "iters": 5, - "median_ms": 49307.6, - "result_size": 3997962, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 2783.1, - "iters": 3, - "median_ms": 1110.9, - "result_size": 3997962, - "warmups": 1 - }, - "graphframes": { - "cold_load_ms": 10255.6, - "iters": 5, - "median_ms": 16336.0, - "result_size": 3997962, - "warmups": 2 - } - } - } - }, - "orkut": { - "n_edges": 117185083, - "n_nodes": 3072441, - "tasks": { - "filter": { - "gfql-polars": { - "cold_load_ms": 5145.3, - "iters": 5, - "median_ms": 1.7, - "result_size": 308666, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 4916.8, - "iters": 5, - "median_ms": 2.0, - "result_size": 308666, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 14725.6, - "iters": 5, - "median_ms": 70.6, - "result_size": 308666, - "warmups": 2 - } - }, - "hop1": { - "gfql-polars": { - "cold_load_ms": 5145.3, - "iters": 5, - "median_ms": 562.9, - "result_size": 434973, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 4916.8, - "iters": 5, - "median_ms": 442.0, - "result_size": 434973, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 14725.6, - "iters": 5, - "median_ms": 3826.6, - "result_size": 434973, - "warmups": 2 - } - }, - "hop2": { - "gfql-polars": { - "cold_load_ms": 5145.3, - "iters": 5, - "median_ms": 9439.8, - "result_size": 1991366, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 4916.8, - "iters": 5, - "median_ms": 8860.2, - "result_size": 1991366, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 14725.6, - "iters": 5, - "median_ms": 11582.9, - "result_size": 1991366, - "warmups": 2 - } - }, - "pagerank": { - "gfql-polars": { - "cold_load_ms": 5145.3, - "iters": 5, - "median_ms": 160097.1, - "result_size": 3072441, - "warmups": 2 - }, - "gfql-polars-gpu": { - "cold_load_ms": 4916.8, - "iters": 5, - "median_ms": 3502.8, - "result_size": 3072441, - "warmups": 2 - }, - "graphframes": { - "cold_load_ms": 14725.6, - "iters": 5, - "median_ms": 36826.0, - "result_size": 3072441, - "warmups": 2 - } - } - } - } -} \ No newline at end of file diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 4148635768..0237b60f31 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -6,102 +6,110 @@ GFQL vs Apache Spark GraphFrames on One Node :width: 160px :align: right -This page benchmarks GFQL against Apache Spark GraphFrames on one machine. -GFQL is Graphistry's open-source graph query language: Cypher and Python -chains that run in-process on dataframes, with no database or cluster. -GraphFrames is Spark's graph library, run here on ``local[*]`` (a single-node -JVM using all cores). The workload is four tasks on two graphs, LiveJournal -(35M edges) and Orkut (117M edges). GFQL's best engine is faster than -GraphFrames in all eight cells. The CPU engine alone is faster in six of the -eight: graph filters and k-hop traversals run 1.3x to 43x faster. The two -exceptions are whole-graph PageRank on CPU, where GraphFrames beats GFQL's -igraph path. On that task the GFQL GPU engine is 10x to 15x faster than -GraphFrames. Use GFQL on CPU for filters and traversals, and on GPU for -PageRank. +This page compares GFQL with Apache Spark GraphFrames on one machine. GFQL is +Graphistry's open-source graph query language: Cypher and Python chains that run +in-process on dataframes, with no database or cluster. GraphFrames is Spark's graph +library, run here on ``local[*]``, a single-node JVM using all cores. The workload is +four tasks on two SNAP graphs, LiveJournal and Orkut, with Friendster as the +larger-than-memory rung the ladder climbs next. Every number below renders from a +committed pyg-bench receipt; the Measurement block at the end names the runs, hosts, and +commits. + +**Where it stands.** On whole-graph PageRank, GFQL on the GPU is +:bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` faster than GraphFrames +on LiveJournal and :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` on +Orkut, and the PageRank solver itself is a small part of that GFQL time (the shaded +bars). The GFQL filter and k-hop rows on this page are the released code's, and they +carry a loss: the released undirected multi-hop path is ~30x slower than it was in June +because of a per-edge Python loop +(`#2023 `_). The fix is under +review (`#2024 `_); the filter and +hop rows are re-measured against it when it lands, and Friendster runs after that. .. image:: _static/graphframes/livejournal_tasks.svg - :alt: LiveJournal task times: GFQL CPU and GPU versus GraphFrames for filter, 1-hop, 2-hop, and PageRank + :alt: LiveJournal task times: GFQL and GraphFrames for filter, 1-hop, 2-hop, and PageRank, with the PageRank solver time shaded inside the GFQL bar .. image:: _static/graphframes/orkut_tasks.svg - :alt: Orkut task times: GFQL CPU and GPU versus GraphFrames for filter, 1-hop, 2-hop, and PageRank + :alt: Orkut task times: GFQL and GraphFrames for filter, 1-hop, 2-hop, and PageRank, with the PageRank solver time shaded inside the GFQL bar -GFQL runs with ``engine="polars"`` (CPU) and ``engine="polars-gpu"`` (GPU). -Every cell is the median of 5 timed runs after 2 warmups, and every task -returns the same result size on all three systems. One cell, LiveJournal GPU -PageRank, is the median of 3 runs after 1 warmup. See -:ref:`graphframes-method` for the full measurement rules. +GFQL binds each graph from a lazy Polars scan of the edge parquet and runs the filter +and hop tasks with ``engine="polars"`` under the Polars CPU streaming collect, or with +``engine="polars-gpu"`` under the cudf-polars streaming executor. PageRank re-binds an +eager cuDF copy outside the timer and calls cuGraph. Every cell is the median of 5 timed +runs after 2 warmups, and every task returns the same result size on every system that +ran it. Times are milliseconds unless marked; lower is better. -LiveJournal (35M edges) ------------------------ +LiveJournal +----------- .. list-table:: :header-rows: 1 - :widths: 26 18 18 18 20 + :widths: 24 19 19 19 19 * - Task - GFQL polars (CPU) - GFQL polars-gpu (GPU) - GraphFrames (local[*]) - - Best GFQL vs GraphFrames + - GFQL GPU vs GraphFrames * - **filter** (degree >= 42) - - 2.1ms - - 2.4ms - - 90.4ms - - 43x + - :bench-diag:`graphframes_059.lj.filter.gfql_polars` + - :bench-diag:`graphframes_059.lj.filter.gfql_polars_gpu` + - :bench:`graphframes.lj.filter.graphframes` + - pending #2024 * - **1-hop** (50 seeds) - - 236.8ms - - 191.4ms - - 1421.7ms - - 7.4x + - :bench-diag:`graphframes_059.lj.hop1.gfql_polars` + - :bench-diag:`graphframes_059.lj.hop1.gfql_polars_gpu` + - :bench:`graphframes.lj.hop1.graphframes` + - pending #2024 * - **2-hop** (50 seeds) - - 1669.3ms - - 1542.1ms - - 3583.3ms - - 2.3x + - :bench-diag:`graphframes_059.lj.hop2.gfql_polars` + - :bench-diag:`graphframes_059.lj.hop2.gfql_polars_gpu` + - :bench:`graphframes.lj.hop2.graphframes` + - pending #2024 * - **PageRank** (full graph) - - 49.3s - - 1.11s - - 16.3s - - 14.7x (GPU); CPU is 0.33x + - not measured (CPU PageRank routes through igraph) + - :bench:`graphframes.lj.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_gpu_kernel`) + - :bench:`graphframes.lj.pagerank.graphframes` + - :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` -Cold load of the SNAP file: 2.4s for GFQL, 10.3s for GraphFrames. +The GFQL filter and hop cells are marked diagnostic: they are the released code with +#2023 in it, kept as the before-state rather than quoted as GFQL's number. The 2-hop +row is the loss; GraphFrames is faster there today. -Orkut (117M edges) ------------------- +Orkut +----- .. list-table:: :header-rows: 1 - :widths: 26 18 18 18 20 + :widths: 24 19 19 19 19 * - Task - GFQL polars (CPU) - GFQL polars-gpu (GPU) - GraphFrames (local[*]) - - Best GFQL vs GraphFrames + - GFQL GPU vs GraphFrames * - **filter** (degree >= 162) - - 1.7ms - - 2.0ms - - 70.6ms - - 42x + - pending #2024 + - pending #2024 + - :bench:`graphframes.orkut.filter.graphframes` + - pending #2024 * - **1-hop** (50 seeds) - - 562.9ms - - 442.0ms - - 3826.6ms - - 8.7x + - pending #2024 + - pending #2024 + - :bench:`graphframes.orkut.hop1.graphframes` + - pending #2024 * - **2-hop** (50 seeds) - - 9439.8ms - - 8860.2ms - - 11582.9ms - - 1.3x + - pending #2024 + - pending #2024 + - :bench:`graphframes.orkut.hop2.graphframes` + - pending #2024 * - **PageRank** (full graph) - - 160.1s - - 3.50s - - 36.8s - - 10.5x (GPU); CPU is 0.23x + - not measured + - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_gpu_kernel`) + - :bench:`graphframes.orkut.pagerank.graphframes` + - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` -Cold load: 5.1s for GFQL, 14.7s for GraphFrames. - -Result sizes are identical across the three systems for every task: +Result sizes agree across the systems that ran each task, as recorded in the receipts: .. list-table:: :header-rows: 1 @@ -126,22 +134,22 @@ Result sizes are identical across the three systems for every task: Which engine to use ------------------- -- **Filter and traversal**: use GFQL on CPU. The GPU changes these times - little, because at these result sizes data movement, not compute, sets the - floor. Spark's per-query scheduling and shuffle cost dominates its time. -- **Whole-graph PageRank**: use GFQL on GPU (``engine="polars-gpu"``, cugraph). -- **PageRank without a GPU**: GFQL routes the CPU path through single-threaded - igraph. It is 3x to 4x slower than GraphFrames at these sizes, and the gap - grows with graph size. Use it for convenience, not for speed. +- **Whole-graph PageRank**: use GFQL on GPU (``engine="polars-gpu"``, cuGraph). The + solver is a small share of the GFQL time; the rest is the conversion of the edge + frame and the join of scores back onto the nodes, which is where the next gains are. +- **Filter and traversal**: the CPU engine is the right choice once #2024 lands; the + released undirected multi-hop path carries the #2023 loss above. The GPU streaming + executor does not help these tasks at these sizes. +- **PageRank without a GPU**: GFQL routes the CPU path through single-threaded igraph. + It is not measured on this page; use it for convenience, not for speed. - **Larger than one node's memory**: see :ref:`graphframes-friendster`. The tasks --------- -**filter**: keep nodes with ``degree >= threshold``. SNAP graphs have no -attributes, so both systems compute ``degree`` during cold load. The load time -carries that cost, not the query. The shared threshold makes the filter -identical across systems. +**filter**: keep nodes with ``degree >= threshold``. SNAP graphs have no attributes, +so both systems compute ``degree`` during load. The load carries that cost, not the +query. The shared threshold makes the filter identical across systems. .. doc-test: skip @@ -155,8 +163,8 @@ identical across systems. # GraphFrames gf.degrees.filter("degree >= 42").count() -**1-hop** and **2-hop**: undirected expansion from a fixed set of 50 -high-degree seed nodes. +**1-hop** and **2-hop**: undirected expansion from a fixed set of 50 high-degree seed +nodes. .. doc-test: skip @@ -166,94 +174,78 @@ high-degree seed nodes. from graphistry import n, e_undirected g.gfql([n(filter_dict={'id': is_in(seeds)}), e_undirected(hops=1), n()], engine="polars") -GraphFrames has no k-hop primitive. Its ``bfs`` finds shortest paths between -predicates and ``find`` matches a fixed motif. The Spark side therefore expands -with one iterated undirected edge join per hop and ends in ``.count()``. +GraphFrames has no k-hop primitive. Its ``bfs`` finds shortest paths between predicates +and ``find`` matches a fixed motif. The Spark side therefore expands with one iterated +undirected edge join per hop and ends in ``.count()``. -**PageRank**: full graph, damping 0.85. GFQL CPU calls -``g.compute_igraph('pagerank')``. GFQL GPU calls -``g.compute_cugraph('pagerank')``. GraphFrames calls -``gf.pageRank(resetProbability=0.15, maxIter=20)``. The three engines return -the same vertex set and the same ranking. On LiveJournal, pairwise Spearman rho -is 1.00 and the top-100 overlap is 100 of 100 -(``_static/graphframes/bench_graphframes_pagerank_parity.json``). +**PageRank**: full graph, damping 0.85. GFQL GPU calls +``g.compute_cugraph('pagerank')`` on an eager cuDF copy of the graph. GraphFrames calls +``gf.pageRank(resetProbability=0.15, maxIter=20)``. Both return the full vertex set. The +shaded part of a GFQL PageRank bar is the cuGraph solver alone on a graph object built +outside the timer; the light part is the rest of the query. .. _graphframes-friendster: -Friendster (1.8B edges): not measured -------------------------------------- +Friendster (1.8B edges): next rung +---------------------------------- Friendster has 1,806,067,135 edges and 65,608,366 nodes -(`SNAP `_). No system -completed a task on the test node (about 120 GB unified memory): - -- **GFQL polars (CPU)**: the harness loads the full edge list into a pandas - frame (about 29 GB) and makes a second pass for degrees. This exceeds physical - RAM before the query runs. -- **GFQL polars-gpu (GPU)**: a direct cudf edge read also exceeds the roughly 120 GB - unified memory pool. -- **GraphFrames (local[*])**: a 90 GB driver heap swaps and does not finish in - usable time. - -This is a limit of the eager in-memory harness, not a measured engine limit. -GFQL has two opt-in larger-than-memory paths that this benchmark did not use. -``GFQL_POLARS_CPU_STREAMING=1`` selects the Polars streaming engine, which -spills to disk. ``GFQL_POLARS_GPU_EXECUTOR=streaming`` selects the cudf-polars -streaming executor for results larger than device memory. Both need a lazy -source such as ``pl.scan_parquet`` instead of an eager ``pandas.read_parquet``. -Measuring that path at 1.8B edges is follow-up work. +(`SNAP `_). The eager harness that +produced the earlier version of this page could not load it on the test node (about 120 +GB unified memory): a pandas edge frame plus a second pass for degrees exceeds physical +RAM, a direct cuDF read exceeds the unified pool, and a 90 GB Spark driver heap swaps. + +The ladder now binds from ``pl.scan_parquet`` and collects through GFQL's streaming +paths (``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, +``GFQL_POLARS_GPU_EXECUTOR=streaming`` for the cudf-polars streaming executor), with a +peak-memory receipt at every rung. LiveJournal and Orkut are the rungs on this page. +Friendster is not measured yet: its 2-hop from hub seeds would spend hours in the #2023 +loop at the released code, so it runs after #2024 lands. GraphFrames on ``local[*]`` +stays the boundary it hit above. .. _graphframes-method: Method and limits ----------------- -- **Scope**: single node, in memory. ``local[*]`` is Spark's single-node mode. - A cluster amortizes scheduling and shuffle cost across machines and changes - the trade-off at larger scale. Use a Spark cluster when the data already - lives there or the graph exceeds one node's memory, including the streaming - paths above. -- **Timing**: median of 5 runs after 2 warmups per cell. LiveJournal GPU - PageRank is median of 3 after 1 warmup, rerun after a transient GPU fault. - Cold load is timed once, separately. +- **Scope**: single node, in memory. ``local[*]`` is Spark's single-node mode. A cluster + amortizes scheduling and shuffle cost across machines and changes the trade-off at + larger scale. Use a Spark cluster when the data already lives there or the graph + exceeds one node's memory. +- **Timing**: median of 5 runs after 2 warmups per cell, each system loaded once and + resident across iterations. Load is not timed. - **Materialization**: Spark is lazy, so every task ends in ``.count()`` or - ``.vertices.count()``. GFQL materializes with ``len(_nodes)`` or - ``len(_edges)``. -- **Conversion cost**: GFQL holds edges as pandas and converts to Polars inside - the timed region on each call. This counts against GFQL. -- **PageRank convergence**: GraphFrames runs a fixed ``maxIter=20``. igraph - stops at ``eps=1e-3`` and cugraph at ``tol=1e-5``. Times compare - wall-clock to a usable ranking; the rankings agree as shown above. -- **Result parity**: each task returns the same result size on all systems - (table above). A mismatch is treated as a bug, not a result. -- **Run order**: all GFQL cells ran in one block, then all GraphFrames cells, - on a shared machine. Only medians are kept. -- **Errors**: each (system, task) cell records a status on error or OOM and the - matrix continues. Missing pyspark, graphframes, or GPU skips the cell. -- **Environment**: ``dgx-spark`` (GB10 GPU, about 120 GB unified memory); - GraphFrames ``0.8.4-spark3.5-s_2.12``; PySpark ``3.5.1``. - -Reproduce ---------- - -This page renders saved results from ``_static/graphframes/results.json``. The -harness is ``benchmarks/gfql/bench_graphframes.py`` (design notes in -``benchmarks/gfql/bench_graphframes_DESIGN.md``). From ``benchmarks/gfql/``, -with the GraphFrames jar on the Spark classpath via ``GRAPHFRAMES_JAR``: - -.. code-block:: bash - - python bench_graphframes.py --dataset lj \ - --systems gfql-polars,gfql-polars-gpu,graphframes \ - --tasks filter,hop1,hop2,pagerank \ - --filter-threshold 42 --warmups 2 --iters 5 - -Orkut uses ``--dataset orkut --filter-threshold 162``. + ``.vertices.count()``. GFQL materializes with ``len(_nodes)``. +- **Comparability**: a task is comparable only when every system reports the same + result size; a cell that disagrees is published as a direct time with a disclosure. + Cells marked diagnostic are never quoted as GFQL's number. +- **PageRank convergence**: GraphFrames runs a fixed ``maxIter=20``; cuGraph runs to + its default tolerance. Times compare wall-clock to a usable ranking. +- **Receipts**: one rung at a time under a host lock, after two clean checks five + minutes apart; a load monitor samples the host every second and a classifier + invalidates the rung if a process outside the benchmark ran during it. Invalidated + attempts stay in the package under ``stale-attempts/``; one Orkut GraphFrames rung is + valid by reclassification after the classifier learned that Spark's own shutdown + cleanup is the benchmark's process (``RECLASSIFIED.txt`` in the rung). +- **Harness**: the GFQL ladder harness and every receipt live in pyg-bench; the + GraphFrames baseline is ``benchmarks/gfql/bench_graphframes.py --systems graphframes`` + in this repository, run from a host Spark with the GraphFrames assembly jar. + +Provenance +---------- + +Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, +which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` +reject missing, stale, or unpublished values. + +.. bench-provenance:: graphframes-ladder-20260904 graphframes-ladder-059-hops-20260904 + :disclosures: See also -------- - :doc:`engines`: choosing pandas, Polars, cuDF, or Polars-GPU - :doc:`benchmark_filter_pagerank`: GFQL CPU/GPU vs Neo4j + GDS +- :doc:`performance`: the q1–q9 boards against Kuzu, Memgraph, and Neo4j - :doc:`cypher`: Cypher syntax through ``g.gfql("MATCH ...")`` - :doc:`overview`: GFQL design and features diff --git a/docs/test_bench_numbers.py b/docs/test_bench_numbers.py index a9ce683b17..16b64092c3 100644 --- a/docs/test_bench_numbers.py +++ b/docs/test_bench_numbers.py @@ -345,6 +345,72 @@ def test_a_chart_over_an_unpublished_cell_fails(payload): assert 'does not publish' in str(excinfo.value) +def _gf_payload(payload, kernel=True, gpu=True): + """A copy of the vendored artifact carrying a synthetic GraphFrames ladder for lj.""" + synthetic = json.loads(json.dumps(payload)) + for key in [k for k in synthetic['cells'] if k.startswith('graphframes')]: + del synthetic['cells'][key] # the synthetic ladder replaces any vendored one + run = 'graphframes-ladder-test' + synthetic['runs'][run] = dict(next(iter(payload['runs'].values()))) + + def cell(key, value, unit='ms', **extra): + base = {'run': run, 'workload': key, 'engine': 'x', + 'measurement_profile': 'graphframes-tasks-warm-resident', 'value': value, + 'unit': unit, 'decimals': 1, 'status': 'ok', 'comparison_allowed': True, + 'board_quotable': True, 'disclosures': []} + base.update(extra) + synthetic['cells'][key] = base + + for task, cpu, gf in (('filter', 2.0, 90.0), ('hop1', 200.0, 1400.0), + ('pagerank', 3000.0, 16000.0)): + cell('graphframes.lj.{}.gfql_polars'.format(task), cpu) + cell('graphframes.lj.{}.graphframes'.format(task), gf) + cell('graphframes.lj.{}.gfql_polars_vs_graphframes'.format(task), gf / cpu, 'x') + if gpu: + cell('graphframes.lj.{}.gfql_polars_gpu'.format(task), cpu / 2) + if kernel: + cell('graphframes.lj.pagerank.gfql_polars_kernel', 2100.0, status='ok', + comparison_allowed=False, board_quotable=False) + return synthetic + + +def test_graphframes_charts_are_skipped_until_the_ladder_is_published(payload): + if charts.gf_published(payload): + pytest.skip('the vendored artifact publishes the ladder') + assert not any(name in charts.rendered(payload) for name in charts.GF_CHARTS) + + +def test_graphframes_chart_shades_the_kernel_inside_the_query_bar(payload): + svg = charts.render_graphframes('livejournal_tasks.svg', _gf_payload(payload)) + assert '3000.0 ms' in svg and '(solver 2100.0 ms)' in svg + assert 'opacity="0.4"' in svg, 'the query bar is drawn light behind the solid kernel bar' + assert '5.3x faster than GraphFrames' in svg + assert 'solid part is the solver alone' in svg + # Orkut has no synthetic cells: its chart refuses rather than drawing an empty frame. + with pytest.raises(charts.ChartError, match='draws no published cell'): + charts.render_graphframes('orkut_tasks.svg', _gf_payload(payload)) + + +def test_graphframes_chart_marks_an_unmeasured_system_without_a_bar(payload): + svg = charts.render_graphframes('livejournal_tasks.svg', _gf_payload(payload, gpu=False)) + assert svg.count('not measured') == 3 + 3 # GPU on three tasks + hop2 for all three + assert 'opacity="0.4"' in svg + + +def test_graphframes_chart_refuses_a_kernel_above_its_query(payload): + synthetic = _gf_payload(payload) + synthetic['cells']['graphframes.lj.pagerank.gfql_polars_kernel']['value'] = 3000.1 + with pytest.raises(charts.ChartError, match='exceeds its query time'): + charts.render_graphframes('livejournal_tasks.svg', synthetic) + + +def test_graphframes_charts_draw_only_published_cells(payload): + if not charts.gf_published(payload): + pytest.skip('the vendored artifact publishes no ladder yet') + keys = charts.gf_cell_keys(payload) + assert keys and all(key in payload['cells'] for key in keys) + + def test_the_chart_renderer_stays_importable_without_sphinx(): with open(os.path.join(SOURCE_DIR, '_ext', 'gfql_bench_charts.py'), encoding='utf-8') as f: source = f.read() From 53190d01c9f73e5c76efa5d3ea82565c8f081332 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 09:10:55 -0700 Subject: [PATCH 10/29] docs(gfql): GraphFrames charts draw the diagnostic (#2023) rows lightly with a label Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_ext/gfql_bench_charts.py | 27 ++++++++++++++++++- .../_static/graphframes/livejournal_tasks.svg | 24 ++++++++++------- docs/test_bench_numbers.py | 11 ++++++++ 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index cc35f74374..57dcf90a9c 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -235,6 +235,10 @@ def render(name: str, payload: JSONObject) -> str: ('graphframes', 'GraphFrames local[*]', 'neo'), ) GF_BASELINE = 'graphframes' +#: A run published under this prefix holds the same tasks measured on code with a known +#: defect, kept as the before-state; the chart draws it lighter and says so. +GF_DIAG_PREFIX = 'graphframes_059' +GF_DIAG_NOTE = 'diagnostic: released code, #2023' class GFChart(NamedTuple): @@ -269,7 +273,8 @@ def gf_cell_keys(payload: JSONObject) -> list[str]: for task, _ in GF_TASKS: for system, _, _ in GF_SYSTEMS: key = gf_cell_key(chart.dataset, task, system) - for candidate in (key, key + '_kernel', key + '_vs_' + GF_BASELINE): + diag = '{}.{}.{}.{}'.format(GF_DIAG_PREFIX, chart.dataset, task, system) + for candidate in (key, key + '_kernel', key + '_vs_' + GF_BASELINE, diag): if candidate in cells: keys.append(candidate) return keys @@ -316,14 +321,22 @@ def render_graphframes(name: str, payload: JSONObject) -> str: bar_x = PAD + GF_LABEL_W drawn = 0 shaded = False + diagnostic_drawn = False for group_index, (task, task_label) in enumerate(GF_TASKS): group_top = HEADER_H + group_index * group_h out.append('{}'.format( PAD, group_top + 15, _esc(task_label.format(threshold=chart.threshold)))) rows: dict[str, tuple[JSONObject, float]] = {} + diagnostic: set[str] = set() for system, _, _ in GF_SYSTEMS: key = gf_cell_key(chart.dataset, task, system) cell = _gf_optional(payload, key) + if cell is None: + diag_key = '{}.{}.{}.{}'.format(GF_DIAG_PREFIX, chart.dataset, task, system) + cell = _gf_optional(payload, diag_key) + if cell is not None: + diagnostic.add(system) + key = diag_key if cell is not None: rows[system] = (cell, _gf_ms(cell, key)) slowest = max((ms for _, ms in rows.values()), default=0.0) @@ -341,6 +354,16 @@ def render_graphframes(name: str, payload: JSONObject) -> str: cell, ms = rows[system] key = gf_cell_key(chart.dataset, task, system) width = max(MIN_BAR, GF_BAR_MAX * ms / slowest) + if system in diagnostic: + diagnostic_drawn = True + out.append(''.format( + tone, _bar_path(bar_x, bar_top, width, GF_BAR_H))) + out.append('{}' + '{}'.format( + _num(bar_x + width + 8), row_top + 18, + _esc(format_cell(cell)), _esc(GF_DIAG_NOTE))) + drawn += 1 + continue kernel = _gf_optional(payload, key + '_kernel') if kernel is not None: kernel_ms = _gf_ms(kernel, key + '_kernel') @@ -375,6 +398,8 @@ def render_graphframes(name: str, payload: JSONObject) -> str: if shaded: foot = ('Bars are scaled per task; on PageRank the solid part is the solver alone, ' 'the light part the rest of the query. Single node; Spark local[*].') + if diagnostic_drawn: + foot += " Light bars marked diagnostic are the released code with #2023 in it." out.append('{}'.format(PAD, height - 11, _esc(foot))) out.append('') return '\n'.join(out) + '\n' diff --git a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg index 5a9dec7530..b7f767d080 100644 --- a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -8,30 +8,36 @@ filter: degree >= 42 GFQL polars (CPU) -not measured + +27.0 msdiagnostic: released code, #2023 GFQL polars-gpu (GPU) -not measured + +27.9 msdiagnostic: released code, #2023 GraphFrames local[*] 80.4 ms 1-hop from 50 seeds GFQL polars (CPU) -not measured + +222.2 msdiagnostic: released code, #2023 GFQL polars-gpu (GPU) -not measured + +1174.4 msdiagnostic: released code, #2023 GraphFrames local[*] 1460.2 ms 2-hop from 50 seeds GFQL polars (CPU) -not measured + +64273.1 msdiagnostic: released code, #2023 GFQL polars-gpu (GPU) -not measured + +63202.1 msdiagnostic: released code, #2023 GraphFrames local[*] - -3475.8 ms + +3475.8 ms PageRank, full graph GFQL polars (CPU) @@ -43,5 +49,5 @@ GraphFrames local[*] 16311.2 ms -Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. +Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. Light bars marked diagnostic are the released code with #2023 in it. diff --git a/docs/test_bench_numbers.py b/docs/test_bench_numbers.py index 16b64092c3..1260881787 100644 --- a/docs/test_bench_numbers.py +++ b/docs/test_bench_numbers.py @@ -391,6 +391,17 @@ def test_graphframes_chart_shades_the_kernel_inside_the_query_bar(payload): charts.render_graphframes('orkut_tasks.svg', _gf_payload(payload)) +def test_graphframes_chart_draws_diagnostic_prefix_cells_lighter_and_labelled(payload): + synthetic = _gf_payload(payload, gpu=False) + cell = dict(synthetic['cells']['graphframes.lj.filter.gfql_polars']) + cell.update({'value': 27.0, 'board_quotable': False, 'comparison_allowed': False}) + synthetic['cells']['graphframes_059.lj.hop2.gfql_polars'] = cell + svg = charts.render_graphframes('livejournal_tasks.svg', synthetic) + assert '27.0 ms' in svg and charts.GF_DIAG_NOTE in svg + assert 'released code with #2023' in svg + assert svg.count('not measured') == 3 + 2 # GPU on three tasks + hop2 GPU/GraphFrames + + def test_graphframes_chart_marks_an_unmeasured_system_without_a_bar(payload): svg = charts.render_graphframes('livejournal_tasks.svg', _gf_payload(payload, gpu=False)) assert svg.count('not measured') == 3 + 3 # GPU on three tasks + hop2 for all three From d32dfe385b13e926699db27069fe7846b62eddde Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:02:08 -0700 Subject: [PATCH 11/29] docs(gfql): GraphFrames ladder at the #2023 fix head; Friendster as the measured ceiling Vendor pyg-bench's second ladder publication: GFQL filter/hop cells and ratios for LiveJournal and Orkut at the head of #2024 (disclosed as pre-landing in the provenance block), and the Friendster rung (1.8B edges, filter + 1-hop on the CPU streaming path, 106 GB peak of a 119 GB host). The page opens with that ceiling, prints wins and losses side by side (GPU PageRank 18.3x/12.5x; CPU faster on filter and 1-hop; GraphFrames wins 2-hop on both graphs; the GPU streaming executor loses every hop), gains a Friendster table and chart, and keeps the released code's 2-hop as a diagnostic before-state sentence. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 554 +++++++++++++++++- docs/source/_ext/gfql_bench_charts.py | 1 + .../_static/graphframes/friendster_tasks.svg | 43 ++ .../_static/graphframes/livejournal_tasks.svg | 30 +- .../gfql/_static/graphframes/orkut_tasks.svg | 26 +- docs/source/gfql/benchmark_graphframes.rst | 136 +++-- 6 files changed, 701 insertions(+), 89 deletions(-) create mode 100644 docs/source/gfql/_static/graphframes/friendster_tasks.svg diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 5d5b96fd16..b6847088f8 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -2124,6 +2124,118 @@ "value": 2.9, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" }, + "graphframes.friendster.filter.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 3111.7, + "workload": "GraphFrames tasks Friendster filter" + }, + "graphframes.friendster.hop1.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 6123.4, + "workload": "GraphFrames tasks Friendster hop1" + }, + "graphframes.lj.filter.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 26.1, + "workload": "GraphFrames tasks LiveJournal filter" + }, + "graphframes.lj.filter.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 27.1, + "workload": "GraphFrames tasks LiveJournal filter" + }, + "graphframes.lj.filter.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.filter.gfql_polars_gpu", + "graphframes.lj.filter.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 3.0, + "workload": "GraphFrames tasks LiveJournal filter \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.lj.filter.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.filter.gfql_polars", + "graphframes.lj.filter.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 3.1, + "workload": "GraphFrames tasks LiveJournal filter \u2014 gfql-polars vs graphframes" + }, "graphframes.lj.filter.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2131,7 +2243,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2141,6 +2254,84 @@ "value": 80.4, "workload": "GraphFrames tasks LiveJournal filter" }, + "graphframes.lj.hop1.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 219.7, + "workload": "GraphFrames tasks LiveJournal hop1" + }, + "graphframes.lj.hop1.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 1159.6, + "workload": "GraphFrames tasks LiveJournal hop1" + }, + "graphframes.lj.hop1.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.hop1.gfql_polars_gpu", + "graphframes.lj.hop1.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 1.3, + "workload": "GraphFrames tasks LiveJournal hop1 \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.lj.hop1.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.hop1.gfql_polars", + "graphframes.lj.hop1.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 6.6, + "workload": "GraphFrames tasks LiveJournal hop1 \u2014 gfql-polars vs graphframes" + }, "graphframes.lj.hop1.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2148,7 +2339,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2158,6 +2350,84 @@ "value": 1460.2, "workload": "GraphFrames tasks LiveJournal hop1" }, + "graphframes.lj.hop2.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 6478.3, + "workload": "GraphFrames tasks LiveJournal hop2" + }, + "graphframes.lj.hop2.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 7162.5, + "workload": "GraphFrames tasks LiveJournal hop2" + }, + "graphframes.lj.hop2.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.hop2.gfql_polars_gpu", + "graphframes.lj.hop2.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.5, + "workload": "GraphFrames tasks LiveJournal hop2 \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.lj.hop2.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.hop2.gfql_polars", + "graphframes.lj.hop2.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.5, + "workload": "GraphFrames tasks LiveJournal hop2 \u2014 gfql-polars vs graphframes" + }, "graphframes.lj.hop2.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2165,7 +2435,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2181,7 +2452,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2197,7 +2469,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.lj.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -2215,7 +2488,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2236,7 +2510,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2246,6 +2521,84 @@ "value": 16311.2, "workload": "GraphFrames tasks LiveJournal pagerank" }, + "graphframes.orkut.filter.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 59.4, + "workload": "GraphFrames tasks Orkut filter" + }, + "graphframes.orkut.filter.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 62.6, + "workload": "GraphFrames tasks Orkut filter" + }, + "graphframes.orkut.filter.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.filter.gfql_polars_gpu", + "graphframes.orkut.filter.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 1.1, + "workload": "GraphFrames tasks Orkut filter \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.orkut.filter.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.filter.gfql_polars", + "graphframes.orkut.filter.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 1.1, + "workload": "GraphFrames tasks Orkut filter \u2014 gfql-polars vs graphframes" + }, "graphframes.orkut.filter.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2253,7 +2606,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2263,6 +2617,84 @@ "value": 66.1, "workload": "GraphFrames tasks Orkut filter" }, + "graphframes.orkut.hop1.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 514.0, + "workload": "GraphFrames tasks Orkut hop1" + }, + "graphframes.orkut.hop1.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 4037.1, + "workload": "GraphFrames tasks Orkut hop1" + }, + "graphframes.orkut.hop1.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.hop1.gfql_polars_gpu", + "graphframes.orkut.hop1.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.9, + "workload": "GraphFrames tasks Orkut hop1 \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.orkut.hop1.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.hop1.gfql_polars", + "graphframes.orkut.hop1.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 7.1, + "workload": "GraphFrames tasks Orkut hop1 \u2014 gfql-polars vs graphframes" + }, "graphframes.orkut.hop1.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2270,7 +2702,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2280,6 +2713,84 @@ "value": 3632.2, "workload": "GraphFrames tasks Orkut hop1" }, + "graphframes.orkut.hop2.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 27348.3, + "workload": "GraphFrames tasks Orkut hop2" + }, + "graphframes.orkut.hop2.gfql_polars_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 33852.9, + "workload": "GraphFrames tasks Orkut hop2" + }, + "graphframes.orkut.hop2.gfql_polars_gpu_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars-gpu", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.hop2.gfql_polars_gpu", + "graphframes.orkut.hop2.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.3, + "workload": "GraphFrames tasks Orkut hop2 \u2014 gfql-polars-gpu vs graphframes" + }, + "graphframes.orkut.hop2.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.hop2.gfql_polars", + "graphframes.orkut.hop2.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.4, + "workload": "GraphFrames tasks Orkut hop2 \u2014 gfql-polars vs graphframes" + }, "graphframes.orkut.hop2.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2287,7 +2798,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2303,7 +2815,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2319,7 +2832,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.orkut.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -2337,7 +2851,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2358,7 +2873,8 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit 840a780), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd)." + "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", + "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -3126,8 +3642,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T15:58:17Z", - "generated_by": "graphistry/pyg-bench 5a1a1663 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T17:58:54Z", + "generated_by": "graphistry/pyg-bench 895c308d scripts/export_docs_numbers.py", "policy": { "drift_waivers": { "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." @@ -3216,7 +3732,7 @@ "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", "competitor": "Apache Spark GraphFrames", "competitor_version": "graphframes assembly jar 1a4826bc (sha256 in each rung's SOURCE_COMMITS) on PySpark 3.5.3, OpenJDK 1.8.0_492, Spark local[*]", - "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / dataset 90th percentile", + "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (65,608,366 nodes / 1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / 148 (Friendster: the 90th degree percentile)", "host": "dgx-spark (NVIDIA GB10), 20 CPU", "measured_at": "2026-09-04", "perf_lock_held": true, @@ -3231,12 +3747,12 @@ "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", "competitor": "Apache Spark GraphFrames", "competitor_version": "graphframes assembly jar 1a4826bc (sha256 in each rung's SOURCE_COMMITS) on PySpark 3.5.3, OpenJDK 1.8.0_492, Spark local[*]", - "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / dataset 90th percentile", + "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (65,608,366 nodes / 1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / 148 (Friendster: the 90th degree percentile)", "host": "dgx-spark (NVIDIA GB10), 20 CPU", "measured_at": "2026-09-04", "perf_lock_held": true, "pyg_bench_commit": "0f8e213", - "pygraphistry_commit": "840a780", + "pygraphistry_commit": "e951e9a", "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", diff --git a/docs/source/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index 57dcf90a9c..be7802c223 100644 --- a/docs/source/_ext/gfql_bench_charts.py +++ b/docs/source/_ext/gfql_bench_charts.py @@ -250,6 +250,7 @@ class GFChart(NamedTuple): GF_CHARTS: dict[str, GFChart] = OrderedDict(( ('livejournal_tasks.svg', GFChart('lj', 'LiveJournal: 3,997,962 nodes / 34,681,189 edges', 42)), ('orkut_tasks.svg', GFChart('orkut', 'Orkut: 3,072,441 nodes / 117,185,083 edges', 162)), + ('friendster_tasks.svg', GFChart('friendster', 'Friendster: 65,608,366 nodes / 1,806,067,135 edges', 148)), )) diff --git a/docs/source/gfql/_static/graphframes/friendster_tasks.svg b/docs/source/gfql/_static/graphframes/friendster_tasks.svg new file mode 100644 index 0000000000..26dad4e7dc --- /dev/null +++ b/docs/source/gfql/_static/graphframes/friendster_tasks.svg @@ -0,0 +1,43 @@ + +Friendster: 65,608,366 nodes / 1,806,067,135 edges + + + +Friendster: 65,608,366 nodes / 1,806,067,135 edges +Median of 5 after 2 warmups; same result size on every system unless marked. Lower is better. +filter: degree >= 148 + +GFQL polars (CPU) + +3111.7 ms +GFQL polars-gpu (GPU) +not measured +GraphFrames local[*] +not measured +1-hop from 50 seeds + +GFQL polars (CPU) + +6123.4 ms +GFQL polars-gpu (GPU) +not measured +GraphFrames local[*] +not measured +2-hop from 50 seeds + +GFQL polars (CPU) +not measured +GFQL polars-gpu (GPU) +not measured +GraphFrames local[*] +not measured +PageRank, full graph + +GFQL polars (CPU) +not measured +GFQL polars-gpu (GPU) +not measured +GraphFrames local[*] +not measured +Bars are scaled per task. dgx-spark, single node; Spark local[*] over all cores. + diff --git a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg index b7f767d080..e2ba55f30e 100644 --- a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -8,36 +8,36 @@ filter: degree >= 42 GFQL polars (CPU) - -27.0 msdiagnostic: released code, #2023 + +26.1 ms3.1x faster than GraphFrames GFQL polars-gpu (GPU) - -27.9 msdiagnostic: released code, #2023 + +27.1 ms3.0x faster than GraphFrames GraphFrames local[*] 80.4 ms 1-hop from 50 seeds GFQL polars (CPU) - -222.2 msdiagnostic: released code, #2023 + +219.7 ms6.6x faster than GraphFrames GFQL polars-gpu (GPU) - -1174.4 msdiagnostic: released code, #2023 + +1159.6 ms1.3x faster than GraphFrames GraphFrames local[*] 1460.2 ms 2-hop from 50 seeds GFQL polars (CPU) - -64273.1 msdiagnostic: released code, #2023 + +6478.3 ms0.5x of GraphFrames’ speed (slower) GFQL polars-gpu (GPU) - -63202.1 msdiagnostic: released code, #2023 + +7162.5 ms0.5x of GraphFrames’ speed (slower) GraphFrames local[*] - -3475.8 ms + +3475.8 ms PageRank, full graph GFQL polars (CPU) @@ -49,5 +49,5 @@ GraphFrames local[*] 16311.2 ms -Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. Light bars marked diagnostic are the released code with #2023 in it. +Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. diff --git a/docs/source/gfql/_static/graphframes/orkut_tasks.svg b/docs/source/gfql/_static/graphframes/orkut_tasks.svg index cf6e4f3b09..fecf884474 100644 --- a/docs/source/gfql/_static/graphframes/orkut_tasks.svg +++ b/docs/source/gfql/_static/graphframes/orkut_tasks.svg @@ -8,30 +8,36 @@ filter: degree >= 162 GFQL polars (CPU) -not measured + +59.4 ms1.1x faster than GraphFrames GFQL polars-gpu (GPU) -not measured + +62.6 ms1.1x faster than GraphFrames GraphFrames local[*] 66.1 ms 1-hop from 50 seeds GFQL polars (CPU) -not measured + +514.0 ms7.1x faster than GraphFrames GFQL polars-gpu (GPU) -not measured + +4037.1 ms0.9x of GraphFrames’ speed (slower) GraphFrames local[*] - -3632.2 ms + +3632.2 ms 2-hop from 50 seeds GFQL polars (CPU) -not measured + +27348.3 ms0.4x of GraphFrames’ speed (slower) GFQL polars-gpu (GPU) -not measured + +33852.9 ms0.3x of GraphFrames’ speed (slower) GraphFrames local[*] - -10885.8 ms + +10885.8 ms PageRank, full graph GFQL polars (CPU) diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 0237b60f31..68a2e67cca 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -15,16 +15,24 @@ larger-than-memory rung the ladder climbs next. Every number below renders from committed pyg-bench receipt; the Measurement block at the end names the runs, hosts, and commits. -**Where it stands.** On whole-graph PageRank, GFQL on the GPU is +**Where it stands.** The single-server ceiling this ladder measured is Friendster: +1,806,067,135 edges bound from a lazy Polars scan, a degree filter in +:bench:`graphframes.friendster.filter.gfql_polars` and a 1-hop from 50 hub seeds in +:bench:`graphframes.friendster.hop1.gfql_polars` on the CPU streaming path, with resident +memory peaking at 106 GB of the 119 GB host; a 2-hop does not fit, and GraphFrames on +``local[*]`` did not load the graph at all. Below that ceiling the picture is mixed and +both sides are printed: on whole-graph PageRank GFQL on the GPU is :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` faster than GraphFrames on LiveJournal and :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` on -Orkut, and the PageRank solver itself is a small part of that GFQL time (the shaded -bars). The GFQL filter and k-hop rows on this page are the released code's, and they -carry a loss: the released undirected multi-hop path is ~30x slower than it was in June -because of a per-edge Python loop -(`#2023 `_). The fix is under -review (`#2024 `_); the filter and -hop rows are re-measured against it when it lands, and Friendster runs after that. +Orkut, and the solver is a small part of that time (the shaded bars); on degree filters +and 1-hop the CPU engine is faster on both graphs; on 2-hop GraphFrames wins on both +(:bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` and +:bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` of its speed). The GFQL +filter and hop rows were measured at the head of the fix for +`#2023 `_ +(`#2024 `_, not yet on master when +measured); the released code's LiveJournal 2-hop was +:bench-diag:`graphframes_059.lj.hop2.gfql_polars`, the before-state the disclosures keep. .. image:: _static/graphframes/livejournal_tasks.svg :alt: LiveJournal task times: GFQL and GraphFrames for filter, 1-hop, 2-hop, and PageRank, with the PageRank solver time shaded inside the GFQL bar @@ -32,6 +40,9 @@ hop rows are re-measured against it when it lands, and Friendster runs after tha .. image:: _static/graphframes/orkut_tasks.svg :alt: Orkut task times: GFQL and GraphFrames for filter, 1-hop, 2-hop, and PageRank, with the PageRank solver time shaded inside the GFQL bar +.. image:: _static/graphframes/friendster_tasks.svg + :alt: Friendster task times: GFQL CPU streaming filter and 1-hop; 2-hop, PageRank, and GraphFrames not measured + GFQL binds each graph from a lazy Polars scan of the edge parquet and runs the filter and hop tasks with ``engine="polars"`` under the Polars CPU streaming collect, or with ``engine="polars-gpu"`` under the cudf-polars streaming executor. PageRank re-binds an @@ -50,31 +61,32 @@ LiveJournal - GFQL polars (CPU) - GFQL polars-gpu (GPU) - GraphFrames (local[*]) - - GFQL GPU vs GraphFrames + - GFQL CPU vs GraphFrames * - **filter** (degree >= 42) - - :bench-diag:`graphframes_059.lj.filter.gfql_polars` - - :bench-diag:`graphframes_059.lj.filter.gfql_polars_gpu` + - :bench:`graphframes.lj.filter.gfql_polars` + - :bench:`graphframes.lj.filter.gfql_polars_gpu` - :bench:`graphframes.lj.filter.graphframes` - - pending #2024 + - :bench:`graphframes.lj.filter.gfql_polars_vs_graphframes` * - **1-hop** (50 seeds) - - :bench-diag:`graphframes_059.lj.hop1.gfql_polars` - - :bench-diag:`graphframes_059.lj.hop1.gfql_polars_gpu` + - :bench:`graphframes.lj.hop1.gfql_polars` + - :bench:`graphframes.lj.hop1.gfql_polars_gpu` - :bench:`graphframes.lj.hop1.graphframes` - - pending #2024 + - :bench:`graphframes.lj.hop1.gfql_polars_vs_graphframes` * - **2-hop** (50 seeds) - - :bench-diag:`graphframes_059.lj.hop2.gfql_polars` - - :bench-diag:`graphframes_059.lj.hop2.gfql_polars_gpu` + - :bench:`graphframes.lj.hop2.gfql_polars` + - :bench:`graphframes.lj.hop2.gfql_polars_gpu` - :bench:`graphframes.lj.hop2.graphframes` - - pending #2024 + - :bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - not measured (CPU PageRank routes through igraph) - :bench:`graphframes.lj.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_gpu_kernel`) - :bench:`graphframes.lj.pagerank.graphframes` - - :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` + - GPU: :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` -The GFQL filter and hop cells are marked diagnostic: they are the released code with -#2023 in it, kept as the before-state rather than quoted as GFQL's number. The 2-hop -row is the loss; GraphFrames is faster there today. +The GPU streaming executor is slower than the CPU streaming collect on both hops here +(:bench:`graphframes.lj.hop1.gfql_polars_gpu` against +:bench:`graphframes.lj.hop1.gfql_polars`); at these result sizes the work is data +movement, and the GPU column is a loss for traversal. Orkut ----- @@ -87,27 +99,49 @@ Orkut - GFQL polars (CPU) - GFQL polars-gpu (GPU) - GraphFrames (local[*]) - - GFQL GPU vs GraphFrames + - GFQL CPU vs GraphFrames * - **filter** (degree >= 162) - - pending #2024 - - pending #2024 + - :bench:`graphframes.orkut.filter.gfql_polars` + - :bench:`graphframes.orkut.filter.gfql_polars_gpu` - :bench:`graphframes.orkut.filter.graphframes` - - pending #2024 + - :bench:`graphframes.orkut.filter.gfql_polars_vs_graphframes` * - **1-hop** (50 seeds) - - pending #2024 - - pending #2024 + - :bench:`graphframes.orkut.hop1.gfql_polars` + - :bench:`graphframes.orkut.hop1.gfql_polars_gpu` - :bench:`graphframes.orkut.hop1.graphframes` - - pending #2024 + - :bench:`graphframes.orkut.hop1.gfql_polars_vs_graphframes` * - **2-hop** (50 seeds) - - pending #2024 - - pending #2024 + - :bench:`graphframes.orkut.hop2.gfql_polars` + - :bench:`graphframes.orkut.hop2.gfql_polars_gpu` - :bench:`graphframes.orkut.hop2.graphframes` - - pending #2024 + - :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - not measured - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_gpu_kernel`) - :bench:`graphframes.orkut.pagerank.graphframes` - - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` + - GPU: :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` + +Friendster +---------- + +One rung, one attempt, on the Polars CPU streaming path; no other system ran. + +.. list-table:: + :header-rows: 1 + :widths: 30 25 45 + + * - Task + - GFQL polars (CPU) + - Result + * - **filter** (degree >= 148, the 90th percentile) + - :bench:`graphframes.friendster.filter.gfql_polars` + - 6,585,312 nodes + * - **1-hop** (50 seeds) + - :bench:`graphframes.friendster.hop1.gfql_polars` + - 166,615 nodes + * - **2-hop**, **PageRank** + - not attempted + - see :ref:`graphframes-friendster` Result sizes agree across the systems that ran each task, as recorded in the receipts: @@ -130,6 +164,11 @@ Result sizes agree across the systems that ran each task, as recorded in the rec - 434,973 - 1,991,366 - 3,072,441 + * - Friendster (GFQL only) + - 6,585,312 + - 166,615 + - not attempted + - not attempted Which engine to use ------------------- @@ -137,9 +176,12 @@ Which engine to use - **Whole-graph PageRank**: use GFQL on GPU (``engine="polars-gpu"``, cuGraph). The solver is a small share of the GFQL time; the rest is the conversion of the edge frame and the join of scores back onto the nodes, which is where the next gains are. -- **Filter and traversal**: the CPU engine is the right choice once #2024 lands; the - released undirected multi-hop path carries the #2023 loss above. The GPU streaming - executor does not help these tasks at these sizes. +- **Filter and 1-hop**: use GFQL on CPU (``engine="polars"``). It is faster than + GraphFrames on both graphs, and the GPU streaming executor does not help at these + result sizes. +- **2-hop from hub seeds**: GraphFrames wins on both graphs today. GFQL's cost is the + wavefront seed-rediscovery rule evaluated over the traversed ball; #2024 removed the + interpreter loop, and the remaining gap is the rule itself. - **PageRank without a GPU**: GFQL routes the CPU path through single-threaded igraph. It is not measured on this page; use it for convenience, not for speed. - **Larger than one node's memory**: see :ref:`graphframes-friendster`. @@ -186,22 +228,26 @@ outside the timer; the light part is the rest of the query. .. _graphframes-friendster: -Friendster (1.8B edges): next rung ----------------------------------- +Friendster (1.8B edges): the ceiling +------------------------------------ Friendster has 1,806,067,135 edges and 65,608,366 nodes (`SNAP `_). The eager harness that -produced the earlier version of this page could not load it on the test node (about 120 +produced the earlier version of this page could not load it on the test node (about 119 GB unified memory): a pandas edge frame plus a second pass for degrees exceeds physical RAM, a direct cuDF read exceeds the unified pool, and a 90 GB Spark driver heap swaps. -The ladder now binds from ``pl.scan_parquet`` and collects through GFQL's streaming -paths (``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, +The ladder binds from ``pl.scan_parquet`` and collects through GFQL's streaming paths +(``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, ``GFQL_POLARS_GPU_EXECUTOR=streaming`` for the cudf-polars streaming executor), with a -peak-memory receipt at every rung. LiveJournal and Orkut are the rungs on this page. -Friendster is not measured yet: its 2-hop from hub seeds would spend hours in the #2023 -loop at the released code, so it runs after #2024 lands. GraphFrames on ``local[*]`` -stays the boundary it hit above. +peak-memory receipt at every rung. On Friendster the CPU streaming rung loaded the graph +(scan plus degree pass in about 20 seconds, 56 GB resident), answered the degree filter +and the 1-hop from 50 hub seeds (table above), and peaked at 106 GB resident after the +1-hop. The Orkut receipt puts 2-hop at 17 GB for 117M edges, and the 2-hop ball from hub +seeds grows with the graph, so 2-hop and PageRank were not attempted: the streaming +collect keeps the load out of memory, but the traversal still materializes the edges it +touches. That is the single-server ceiling this page measured. GraphFrames on +``local[*]`` stays at the boundary it hit above. .. _graphframes-method: From eea6aecde6f5cc2d2d472d626948496b46912c5d Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:07:33 -0700 Subject: [PATCH 12/29] docs(gfql): re-vendor ladder provenance (Friendster RMM cap named) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index b6847088f8..c8a6c154ce 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -3642,8 +3642,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T17:58:54Z", - "generated_by": "graphistry/pyg-bench 895c308d scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T18:07:12Z", + "generated_by": "graphistry/pyg-bench a0d9ee2a scripts/export_docs_numbers.py", "policy": { "drift_waivers": { "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." @@ -3741,7 +3741,7 @@ "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "graphframes-ladder-20260904": { "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", @@ -3756,7 +3756,7 @@ "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", From d8b3fd61c0c31161e98f8b61200a3a4e329737da Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:34:54 -0700 Subject: [PATCH 13/29] docs(gfql): case study prose names no release version for a measurement (T2.9.10) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/gfql/benchmark_filter_pagerank.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index c17cb11584..38ce2e315f 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -144,7 +144,7 @@ Method and limits the locked lane's set; the GPlus GPU arm is at 0.91 against CPU and is therefore diagnostic-only (see the caveats below). - **Timing**: warm runs after warm-up (2 warm-ups, 5 timed runs, median). The - GFQL arms were measured at the 0.59.0 release commit; the Twitter Neo4j arm + GFQL arms were measured at the release commit named in the Measurement block; the Twitter Neo4j arm is the 2026-07-28 measurement, and the GPlus Neo4j time comes from a later locked run of twelve position-balanced slots on one machine. The Measurement block below records every run. From 43fb3d6f686a0f71c447bc527d037692595c83ab Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:39:29 -0700 Subject: [PATCH 14/29] docs(gfql): no nested parentheses around the diagnostic solver cells Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/gfql/benchmark_graphframes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 68a2e67cca..e9740fcb9e 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -79,7 +79,7 @@ LiveJournal - :bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - not measured (CPU PageRank routes through igraph) - - :bench:`graphframes.lj.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_gpu_kernel`) + - :bench:`graphframes.lj.pagerank.gfql_polars_gpu`; solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_gpu_kernel` - :bench:`graphframes.lj.pagerank.graphframes` - GPU: :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` @@ -117,7 +117,7 @@ Orkut - :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - not measured - - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu` (solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_gpu_kernel`) + - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu`; solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_gpu_kernel` - :bench:`graphframes.orkut.pagerank.graphframes` - GPU: :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` From d165342a0b885d6fc848084a1f8496161a7f7e73 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:47:40 -0700 Subject: [PATCH 15/29] docs(gfql): plain words for the size series; drop the engine-error sentence from the performance lede (review round 4: A5, B2) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/gfql/benchmark_graphframes.rst | 20 ++++++++++---------- docs/source/gfql/performance.rst | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index e9740fcb9e..231bb21334 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -11,11 +11,11 @@ Graphistry's open-source graph query language: Cypher and Python chains that run in-process on dataframes, with no database or cluster. GraphFrames is Spark's graph library, run here on ``local[*]``, a single-node JVM using all cores. The workload is four tasks on two SNAP graphs, LiveJournal and Orkut, with Friendster as the -larger-than-memory rung the ladder climbs next. Every number below renders from a +larger-than-memory size measured last. Every number below renders from a committed pyg-bench receipt; the Measurement block at the end names the runs, hosts, and commits. -**Where it stands.** The single-server ceiling this ladder measured is Friendster: +**Where it stands.** The single-server ceiling measured here is Friendster: 1,806,067,135 edges bound from a lazy Polars scan, a degree filter in :bench:`graphframes.friendster.filter.gfql_polars` and a 1-hop from 50 hub seeds in :bench:`graphframes.friendster.hop1.gfql_polars` on the CPU streaming path, with resident @@ -124,7 +124,7 @@ Orkut Friendster ---------- -One rung, one attempt, on the Polars CPU streaming path; no other system ran. +One attempt, on the Polars CPU streaming path; no other system ran. .. list-table:: :header-rows: 1 @@ -237,10 +237,10 @@ produced the earlier version of this page could not load it on the test node (ab GB unified memory): a pandas edge frame plus a second pass for degrees exceeds physical RAM, a direct cuDF read exceeds the unified pool, and a 90 GB Spark driver heap swaps. -The ladder binds from ``pl.scan_parquet`` and collects through GFQL's streaming paths +The harness binds from ``pl.scan_parquet`` and collects through GFQL's streaming paths (``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, ``GFQL_POLARS_GPU_EXECUTOR=streaming`` for the cudf-polars streaming executor), with a -peak-memory receipt at every rung. On Friendster the CPU streaming rung loaded the graph +peak-memory receipt at every size. On Friendster the CPU streaming run loaded the graph (scan plus degree pass in about 20 seconds, 56 GB resident), answered the degree filter and the 1-hop from 50 hub seeds (table above), and peaked at 106 GB resident after the 1-hop. The Orkut receipt puts 2-hop at 17 GB for 117M edges, and the 2-hop ball from hub @@ -267,13 +267,13 @@ Method and limits Cells marked diagnostic are never quoted as GFQL's number. - **PageRank convergence**: GraphFrames runs a fixed ``maxIter=20``; cuGraph runs to its default tolerance. Times compare wall-clock to a usable ranking. -- **Receipts**: one rung at a time under a host lock, after two clean checks five +- **Receipts**: one run at a time under a host lock, after two clean checks five minutes apart; a load monitor samples the host every second and a classifier - invalidates the rung if a process outside the benchmark ran during it. Invalidated - attempts stay in the package under ``stale-attempts/``; one Orkut GraphFrames rung is + invalidates the run if a process outside the benchmark ran during it. Invalidated + attempts stay in the package under ``stale-attempts/``; one Orkut GraphFrames run is valid by reclassification after the classifier learned that Spark's own shutdown - cleanup is the benchmark's process (``RECLASSIFIED.txt`` in the rung). -- **Harness**: the GFQL ladder harness and every receipt live in pyg-bench; the + cleanup is the benchmark's process (``RECLASSIFIED.txt`` in that run's directory). +- **Harness**: the GFQL streaming harness and every receipt live in pyg-bench; the GraphFrames baseline is ``benchmarks/gfql/bench_graphframes.py --systems graphframes`` in this repository, run from a host Spark with the GraphFrames assembly jar. diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index febd49e440..dc935a8b6d 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -11,8 +11,8 @@ Choose an engine ---------------- GFQL runs the same query on ``pandas`` (the default), ``polars`` (CPU), ``cudf`` -(NVIDIA GPU), or ``polars-gpu``. Each engine returns the same rows, or GFQL reports an -error before execution instead of changing engines. On the q1–q9 boards below, the +(NVIDIA GPU), or ``polars-gpu``, and every engine returns the same rows. On the q1–q9 +boards below, the Polars engine is faster than pandas on :bench-tally:`graphbench.100k|polars|pandas` queries at 100,000 people, by up to :bench:`graphbench.100k.q6.polars_vs_pandas` (q6). See :doc:`engines` for the selection guide. From 6c8b782e67551bc92049fede7823eb3c243ba94c Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 11:53:06 -0700 Subject: [PATCH 16/29] =?UTF-8?q?docs(gfql):=20bench-board=20directive=20?= =?UTF-8?q?=E2=80=94=20fastest=20cell=20per=20row=20in=20bold=20plus=20a?= =?UTF-8?q?=20Fastest=20column?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 4: the q1-q9 and SNB boards made the reader compare digits to see who won. bench-board renders a board from published cells (same gate as the :bench: role), bolds the fastest cell on each row, names it in a last column, and prints a dash for a system that has no number for that query. The four boards on the performance page now use it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/.rstcheck.cfg | 1 + docs/source/_ext/gfql_bench.py | 101 +++++++++++++ docs/source/gfql/performance.rst | 250 ++----------------------------- 3 files changed, 116 insertions(+), 236 deletions(-) diff --git a/docs/.rstcheck.cfg b/docs/.rstcheck.cfg index ada8b4c969..a0e60ac1b5 100644 --- a/docs/.rstcheck.cfg +++ b/docs/.rstcheck.cfg @@ -48,6 +48,7 @@ ignore_directives = include, graphviz, bench-provenance, + bench-board, bench-disclosures # Ignore common informational messages diff --git a/docs/source/_ext/gfql_bench.py b/docs/source/_ext/gfql_bench.py index 73d6388ca3..2d95e09abf 100644 --- a/docs/source/_ext/gfql_bench.py +++ b/docs/source/_ext/gfql_bench.py @@ -161,6 +161,106 @@ def run(self) -> List[nodes.Node]: return [_admonition('Measurement', field_list)] +class BenchBoard(Directive): + """A results table from published cells with the fastest cell per row in bold. + + :: + + .. bench-board:: graphbench.100k + :rows: q1,q2,q3 + :columns: kuzu=Kuzu, polars=GFQL polars + :diagnostic: gfql_polars_059 + :units: ms + + Every cell is ``..`` looked up through the same gate as the + ``:bench:`` role (a column named under ``:diagnostic:`` goes through ``:bench-diag:``). + A missing cell renders as a dash, is not a candidate for fastest, and is not an error: + a database that cannot run a query has no number. The last column names the fastest + system on the row, so a win or a loss is read off without comparing digits. + """ + + required_arguments = 1 + optional_arguments = 0 + has_content = False + option_spec = { + 'rows': directives.unchanged_required, + 'columns': directives.unchanged_required, + 'diagnostic': directives.unchanged, + 'row-labels': directives.unchanged, + } + + def run(self) -> List[nodes.Node]: + state = _state() + docname = self.state.document.settings.env.docname + prefix = self.arguments[0].strip() + rows = [r.strip() for r in self.options['rows'].split(',') if r.strip()] + columns = [] # type: List[Tuple[str, str]] + for item in self.options['columns'].split(','): + key, _, label = item.partition('=') + columns.append((key.strip(), (label or key).strip())) + diagnostic = {c.strip() for c in self.options.get('diagnostic', '').split(',') if c.strip()} + labels = {} # type: Dict[str, str] + for item in self.options.get('row-labels', '').split(';'): + key, _, label = item.partition('=') + if key.strip(): + labels[key.strip()] = label.strip() + before = len(state.problems) + table = nodes.table() + tgroup = nodes.tgroup(cols=len(columns) + 2) + table += tgroup + for _ in range(len(columns) + 2): + tgroup += nodes.colspec(colwidth=1) + thead = nodes.thead() + tgroup += thead + thead += _row([nodes.paragraph(text=t) for t in ['Query'] + [c[1] for c in columns] + ['Fastest']]) + tbody = nodes.tbody() + tgroup += tbody + for row in rows: + cells = [] # type: List[Tuple[str, Optional[JSONObject]]] + for key, _ in columns: + cell_key = '{}.{}.{}'.format(prefix, row, key) + cell = state.cell(cell_key) + if cell is None: + cells.append((key, None)) + continue + cells.append((key, check_reference(state, cell_key, docname, self.lineno, key in diagnostic))) + values = [(key, cell['value']) for key, cell in cells + if cell is not None and isinstance(cell['value'], (int, float)) and key not in diagnostic] + fastest = min(values, key=lambda kv: kv[1])[0] if values else None + entries = [nodes.paragraph(text=labels.get(row, row))] + for key, cell in cells: + if cell is None: + entries.append(nodes.paragraph(text='\u2014')) + continue + text = format_cell(cell) + if key in diagnostic: + text += ' (diagnostic)' + literal = nodes.literal(text, text) + para = nodes.paragraph() + if key == fastest and len(values) > 1: + strong = nodes.strong() + strong += literal + para += strong + else: + para += literal + entries.append(para) + fastest_label = dict(columns).get(fastest, '') if fastest and len(values) > 1 else '\u2014' + entries.append(nodes.paragraph(text=fastest_label)) + tbody += _row(entries) + for message in state.problems[before:]: + logger.warning('[gfql-bench] %s', message) + return [table] + + +def _row(entries: List[nodes.Node]) -> nodes.row: + row = nodes.row() + for entry in entries: + cell = nodes.entry() + cell += entry + row += cell + return row + + class BenchDisclosures(Directive): """Render every disclosure attached to a number this page prints.""" @@ -290,6 +390,7 @@ def setup(app: Sphinx) -> Dict[str, object]: app.add_role('bench-tally', _tally_role) app.add_directive('bench-provenance', BenchProvenance) app.add_directive('bench-disclosures', BenchDisclosures) + app.add_directive('bench-board', BenchBoard) app.connect('builder-inited', _on_builder_inited) app.connect('env-purge-doc', _on_purge) app.connect('build-finished', _on_build_finished) diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index dc935a8b6d..6976996455 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -58,178 +58,16 @@ interest side, which GFQL's Cypher path does not yet do. The 20,000-person board ~~~~~~~~~~~~~~~~~~~~~~~ -.. list-table:: - :header-rows: 1 - :widths: 8 8 12 12 12 16 16 16 - - * - Query - - Rows - - Kuzu - - Memgraph - - Neo4j - - GFQL ``pandas`` - - GFQL ``polars`` - - GFQL ``polars-gpu`` - * - q1 - - 3 - - :bench:`graphbench.20k.q1.kuzu` - - :bench:`graphbench.20k.q1.memgraph` - - :bench:`graphbench.20k.q1.neo4j` - - :bench:`graphbench.20k.q1.pandas` - - :bench:`graphbench.20k.q1.polars` - - :bench:`graphbench.20k.q1.polars_gpu` - * - q2 - - 1 - - :bench:`graphbench.20k.q2.kuzu` - - :bench:`graphbench.20k.q2.memgraph` - - :bench:`graphbench.20k.q2.neo4j` - - :bench:`graphbench.20k.q2.pandas` - - :bench:`graphbench.20k.q2.polars` - - :bench:`graphbench.20k.q2.polars_gpu` - * - q3 - - 5 - - :bench:`graphbench.20k.q3.kuzu` - - :bench:`graphbench.20k.q3.memgraph` - - :bench:`graphbench.20k.q3.neo4j` - - :bench:`graphbench.20k.q3.pandas` - - :bench:`graphbench.20k.q3.polars` - - :bench:`graphbench.20k.q3.polars_gpu` - * - q4 - - 2 - - :bench:`graphbench.20k.q4.kuzu` - - :bench:`graphbench.20k.q4.memgraph` - - :bench:`graphbench.20k.q4.neo4j` - - :bench:`graphbench.20k.q4.pandas` - - :bench:`graphbench.20k.q4.polars` - - :bench:`graphbench.20k.q4.polars_gpu` - * - q5 - - 1 - - :bench:`graphbench.20k.q5.kuzu` - - :bench:`graphbench.20k.q5.memgraph` - - :bench:`graphbench.20k.q5.neo4j` - - :bench:`graphbench.20k.q5.pandas` - - :bench:`graphbench.20k.q5.polars` - - :bench:`graphbench.20k.q5.polars_gpu` - * - q6 - - 5 - - :bench:`graphbench.20k.q6.kuzu` - - :bench:`graphbench.20k.q6.memgraph` - - :bench:`graphbench.20k.q6.neo4j` - - :bench:`graphbench.20k.q6.pandas` - - :bench:`graphbench.20k.q6.polars` - - :bench:`graphbench.20k.q6.polars_gpu` - * - q7 - - 1 - - :bench:`graphbench.20k.q7.kuzu` - - :bench:`graphbench.20k.q7.memgraph` - - :bench:`graphbench.20k.q7.neo4j` - - :bench:`graphbench.20k.q7.pandas` - - :bench:`graphbench.20k.q7.polars` - - :bench:`graphbench.20k.q7.polars_gpu` - * - q8 - - 1 - - :bench:`graphbench.20k.q8.kuzu` - - :bench:`graphbench.20k.q8.memgraph` - - :bench:`graphbench.20k.q8.neo4j` - - :bench:`graphbench.20k.q8.pandas` - - :bench:`graphbench.20k.q8.polars` - - :bench:`graphbench.20k.q8.polars_gpu` - * - q9 - - 1 - - :bench:`graphbench.20k.q9.kuzu` - - :bench:`graphbench.20k.q9.memgraph` - - :bench:`graphbench.20k.q9.neo4j` - - :bench:`graphbench.20k.q9.pandas` - - :bench:`graphbench.20k.q9.polars` - - :bench:`graphbench.20k.q9.polars_gpu` +.. bench-board:: graphbench.20k + :rows: q1,q2,q3,q4,q5,q6,q7,q8,q9 + :columns: kuzu=Kuzu, memgraph=Memgraph, neo4j=Neo4j, pandas=GFQL pandas, polars=GFQL polars, polars_gpu=GFQL polars-gpu The 100,000-person board ~~~~~~~~~~~~~~~~~~~~~~~~ -.. list-table:: - :header-rows: 1 - :widths: 8 8 12 12 12 16 16 16 - - * - Query - - Rows - - Kuzu - - Memgraph - - Neo4j - - GFQL ``pandas`` - - GFQL ``polars`` - - GFQL ``polars-gpu`` - * - q1 - - 3 - - :bench:`graphbench.100k.q1.kuzu` - - :bench:`graphbench.100k.q1.memgraph` - - :bench:`graphbench.100k.q1.neo4j` - - :bench:`graphbench.100k.q1.pandas` - - :bench:`graphbench.100k.q1.polars` - - :bench:`graphbench.100k.q1.polars_gpu` - * - q2 - - 1 - - :bench:`graphbench.100k.q2.kuzu` - - :bench:`graphbench.100k.q2.memgraph` - - :bench:`graphbench.100k.q2.neo4j` - - :bench:`graphbench.100k.q2.pandas` - - :bench:`graphbench.100k.q2.polars` - - :bench:`graphbench.100k.q2.polars_gpu` - * - q3 - - 5 - - :bench:`graphbench.100k.q3.kuzu` - - :bench:`graphbench.100k.q3.memgraph` - - :bench:`graphbench.100k.q3.neo4j` - - :bench:`graphbench.100k.q3.pandas` - - :bench:`graphbench.100k.q3.polars` - - :bench:`graphbench.100k.q3.polars_gpu` - * - q4 - - 3 - - :bench:`graphbench.100k.q4.kuzu` - - :bench:`graphbench.100k.q4.memgraph` - - :bench:`graphbench.100k.q4.neo4j` - - :bench:`graphbench.100k.q4.pandas` - - :bench:`graphbench.100k.q4.polars` - - :bench:`graphbench.100k.q4.polars_gpu` - * - q5 - - 1 - - :bench:`graphbench.100k.q5.kuzu` - - :bench:`graphbench.100k.q5.memgraph` - - :bench:`graphbench.100k.q5.neo4j` - - :bench:`graphbench.100k.q5.pandas` - - :bench:`graphbench.100k.q5.polars` - - :bench:`graphbench.100k.q5.polars_gpu` - * - q6 - - 5 - - :bench:`graphbench.100k.q6.kuzu` - - :bench:`graphbench.100k.q6.memgraph` - - :bench:`graphbench.100k.q6.neo4j` - - :bench:`graphbench.100k.q6.pandas` - - :bench:`graphbench.100k.q6.polars` - - :bench:`graphbench.100k.q6.polars_gpu` - * - q7 - - 1 - - :bench:`graphbench.100k.q7.kuzu` - - :bench:`graphbench.100k.q7.memgraph` - - :bench:`graphbench.100k.q7.neo4j` - - :bench:`graphbench.100k.q7.pandas` - - :bench:`graphbench.100k.q7.polars` - - :bench:`graphbench.100k.q7.polars_gpu` - * - q8 - - 1 - - :bench:`graphbench.100k.q8.kuzu` - - :bench:`graphbench.100k.q8.memgraph` - - :bench:`graphbench.100k.q8.neo4j` - - :bench:`graphbench.100k.q8.pandas` - - :bench:`graphbench.100k.q8.polars` - - :bench:`graphbench.100k.q8.polars_gpu` - * - q9 - - 1 - - :bench:`graphbench.100k.q9.kuzu` - - :bench:`graphbench.100k.q9.memgraph` - - :bench:`graphbench.100k.q9.neo4j` - - :bench:`graphbench.100k.q9.pandas` - - :bench:`graphbench.100k.q9.polars` - - :bench:`graphbench.100k.q9.polars_gpu` +.. bench-board:: graphbench.100k + :rows: q1,q2,q3,q4,q5,q6,q7,q8,q9 + :columns: kuzu=Kuzu, memgraph=Memgraph, neo4j=Neo4j, pandas=GFQL pandas, polars=GFQL polars, polars_gpu=GFQL polars-gpu .. _gfql-snb-aligned: @@ -251,78 +89,18 @@ point lookups. SF0.1 ~~~~~ -.. list-table:: - :header-rows: 1 - :widths: 24 19 19 19 19 - - * - Query - - GFQL ``polars`` - - Kuzu - - Neo4j - - Memgraph - * - seed lookup - - :bench:`snb.sf01.seed_lookup.gfql_polars` - - :bench:`snb.sf01.seed_lookup.kuzu` - - :bench:`snb.sf01.seed_lookup.neo4j` - - :bench:`snb.sf01.seed_lookup.memgraph` - * - message content - - :bench:`snb.sf01.message_content.gfql_polars` - - :bench:`snb.sf01.message_content.kuzu` - - :bench:`snb.sf01.message_content.neo4j` - - :bench:`snb.sf01.message_content.memgraph` - * - message creator - - :bench:`snb.sf01.message_creator.gfql_polars` - - :bench:`snb.sf01.message_creator.kuzu` - - :bench:`snb.sf01.message_creator.neo4j` - - :bench:`snb.sf01.message_creator.memgraph` - * - recent replies - - :bench:`snb.sf01.recent_replies.gfql_polars` - - :bench:`snb.sf01.recent_replies.kuzu` - - :bench:`snb.sf01.recent_replies.neo4j` - - :bench:`snb.sf01.recent_replies.memgraph` - * - message replies (GFQL and Kuzu only) - - :bench:`snb.sf01.message_replies.gfql_polars` - - :bench:`snb.sf01.message_replies.kuzu` - - - - - * - new topics (GFQL and Kuzu only) - - :bench:`snb.sf01.new_topics.gfql_polars` - - :bench:`snb.sf01.new_topics.kuzu` - - - - +.. bench-board:: snb.sf01 + :rows: seed_lookup,message_content,message_creator,recent_replies,message_replies,new_topics + :columns: gfql_polars=GFQL polars, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph + :row-labels: seed_lookup=seed lookup; message_content=message content; message_creator=message creator; recent_replies=recent replies; message_replies=message replies (GFQL and Kuzu only); new_topics=new topics (GFQL and Kuzu only) SF1 ~~~ -.. list-table:: - :header-rows: 1 - :widths: 24 19 19 19 19 - - * - Query - - GFQL ``polars`` - - Kuzu - - Neo4j - - Memgraph - * - seed lookup - - :bench:`snb.sf1.seed_lookup.gfql_polars` - - :bench:`snb.sf1.seed_lookup.kuzu` - - :bench:`snb.sf1.seed_lookup.neo4j` - - :bench:`snb.sf1.seed_lookup.memgraph` - * - message content - - :bench:`snb.sf1.message_content.gfql_polars` - - :bench:`snb.sf1.message_content.kuzu` - - :bench:`snb.sf1.message_content.neo4j` - - :bench:`snb.sf1.message_content.memgraph` - * - message creator - - :bench:`snb.sf1.message_creator.gfql_polars` - - :bench:`snb.sf1.message_creator.kuzu` - - :bench:`snb.sf1.message_creator.neo4j` - - :bench:`snb.sf1.message_creator.memgraph` - * - new topics (GFQL and Kuzu only) - - :bench:`snb.sf1.new_topics.gfql_polars` - - :bench:`snb.sf1.new_topics.kuzu` - - - - +.. bench-board:: snb.sf1 + :rows: seed_lookup,message_content,message_creator,new_topics + :columns: gfql_polars=GFQL polars, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph + :row-labels: seed_lookup=seed lookup; message_content=message content; message_creator=message creator; recent_replies=recent replies; message_replies=message replies (GFQL and Kuzu only); new_topics=new topics (GFQL and Kuzu only) Neo4j and Memgraph use a reduced adapter for one query, and one parameter returns zero rows; those cells are excluded rather than estimated. SF10 was not run. From eac230176f617388c28228a92cf7b726407e7102 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 12:04:36 -0700 Subject: [PATCH 17/29] docs(gfql): re-vendor (publication acceptance policy + methodology blocks; values unchanged) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index c8a6c154ce..a2a6c412bb 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -3642,9 +3642,10 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T18:07:12Z", - "generated_by": "graphistry/pyg-bench a0d9ee2a scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T19:01:47Z", + "generated_by": "graphistry/pyg-bench 5e6e96c7 scripts/export_docs_numbers.py", "policy": { + "accepted_regressions": [], "drift_waivers": { "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." }, @@ -3665,7 +3666,8 @@ "performance.rst" ], "max_age_days": 60, - "max_compute_commit_drift": 12 + "max_compute_commit_drift": 12, + "max_unexplained_regression_pct": 25 }, "runs": { "filter-pagerank-059-20260904": { From 900851806270792af626fc07f2533a47015db565 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 12:26:32 -0700 Subject: [PATCH 18/29] docs(gfql): SNB boards show GFQL with resident indexes (pandas + polars) and say where the loss comes from Review round 4: the published GFQL SNB column ran without indexes while the databases ran with theirs. The boards now print the indexed pandas and polars arms (pyg-bench snb-aligned-indexed-20260904) next to Kuzu, Neo4j and Memgraph; the prose says the index engages only on the hop-shaped rows (2.5-3x there) and that the point lookups keep a ~20 ms per-call floor in the chain pipeline (#2027). The un-indexed run stays in the provenance block as the before-state. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 351 ++++++++++++++++++++++++- docs/source/gfql/performance.rst | 23 +- 2 files changed, 363 insertions(+), 11 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index a2a6c412bb..74a91e9eb4 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -3076,6 +3076,21 @@ "value": 11.72, "workload": "filter -> PageRank -> filter, twitter \u2014 Neo4j + GDS" }, + "snb.sf01.message_content.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 10.059, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" + }, "snb.sf01.message_content.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3091,6 +3106,21 @@ "value": 2.91, "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" }, + "snb.sf01.message_content.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 3.591, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" + }, "snb.sf01.message_content.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3139,6 +3169,21 @@ "value": 2.26, "workload": "LDBC SNB-derived aligned queries sf0.1 message-content" }, + "snb.sf01.message_creator.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 26.569, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" + }, "snb.sf01.message_creator.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3154,6 +3199,21 @@ "value": 20.352, "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" }, + "snb.sf01.message_creator.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 16.078, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" + }, "snb.sf01.message_creator.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3202,6 +3262,21 @@ "value": 2.63, "workload": "LDBC SNB-derived aligned queries sf0.1 message-creator" }, + "snb.sf01.message_replies.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 13.847, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" + }, "snb.sf01.message_replies.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3217,6 +3292,21 @@ "value": 48.017, "workload": "LDBC SNB-derived aligned queries sf0.1 message-replies" }, + "snb.sf01.message_replies.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 14.668, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" + }, "snb.sf01.message_replies.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3233,6 +3323,21 @@ "value": 23.904, "workload": "LDBC SNB-derived aligned queries sf0.1 message-replies" }, + "snb.sf01.new_topics.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 70.391, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" + }, "snb.sf01.new_topics.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3248,6 +3353,21 @@ "value": 45.86, "workload": "LDBC SNB-derived aligned queries sf0.1 new-topics" }, + "snb.sf01.new_topics.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 43.151, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" + }, "snb.sf01.new_topics.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3264,6 +3384,21 @@ "value": 47.198, "workload": "LDBC SNB-derived aligned queries sf0.1 new-topics" }, + "snb.sf01.recent_replies.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 47.5, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" + }, "snb.sf01.recent_replies.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3279,6 +3414,21 @@ "value": 84.978, "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" }, + "snb.sf01.recent_replies.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 32.129, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" + }, "snb.sf01.recent_replies.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3327,6 +3477,21 @@ "value": 7.454, "workload": "LDBC SNB-derived aligned queries sf0.1 recent-replies" }, + "snb.sf01.seed_lookup.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 30.118, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" + }, "snb.sf01.seed_lookup.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3342,6 +3507,21 @@ "value": 26.706, "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" }, + "snb.sf01.seed_lookup.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 21.817, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" + }, "snb.sf01.seed_lookup.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3390,6 +3570,38 @@ "value": 3.497, "workload": "LDBC SNB-derived aligned queries sf0.1 seed-lookup" }, + "snb.sf01.tag_cooccurrence.gfql_pandas_idx": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "result_mismatch", + "unit": "ms", + "value": 59.065, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" + }, + "snb.sf01.tag_cooccurrence.gfql_polars_idx": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "result_mismatch", + "unit": "ms", + "value": 30.222, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" + }, "snb.sf1.is3.single_query.polars": { "board_quotable": false, "comparison_allowed": false, @@ -3420,6 +3632,21 @@ "value": 75.22, "workload": "LDBC SNB IS3 (SF1), superseded two-pass adapter form" }, + "snb.sf1.message_content.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 67.631, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" + }, "snb.sf1.message_content.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3435,6 +3662,21 @@ "value": 3.556, "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, + "snb.sf1.message_content.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 4.251, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" + }, "snb.sf1.message_content.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3483,6 +3725,21 @@ "value": 2.435, "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, + "snb.sf1.message_creator.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 344.293, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" + }, "snb.sf1.message_creator.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3498,6 +3755,21 @@ "value": 78.38, "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, + "snb.sf1.message_creator.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 43.607, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" + }, "snb.sf1.message_creator.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3546,6 +3818,21 @@ "value": 1.671, "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, + "snb.sf1.new_topics.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 571.088, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" + }, "snb.sf1.new_topics.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3561,6 +3848,21 @@ "value": 201.442, "workload": "LDBC SNB-derived aligned queries sf1 new-topics" }, + "snb.sf1.new_topics.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 163.878, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" + }, "snb.sf1.new_topics.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3577,6 +3879,21 @@ "value": 223.715, "workload": "LDBC SNB-derived aligned queries sf1 new-topics" }, + "snb.sf1.seed_lookup.gfql_pandas_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_pandas_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 359.515, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" + }, "snb.sf1.seed_lookup.gfql_polars": { "board_quotable": true, "comparison_allowed": true, @@ -3592,6 +3909,21 @@ "value": 89.161, "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" }, + "snb.sf1.seed_lookup.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, + "disclosures": [ + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + ], + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-indexed-20260904", + "status": "ok", + "unit": "ms", + "value": 45.793, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" + }, "snb.sf1.seed_lookup.kuzu": { "board_quotable": true, "comparison_allowed": true, @@ -3642,8 +3974,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T19:01:47Z", - "generated_by": "graphistry/pyg-bench 5e6e96c7 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T19:24:23Z", + "generated_by": "graphistry/pyg-bench f6269134 scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -3760,6 +4092,21 @@ "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, + "snb-aligned-indexed-20260904": { + "artifact": "results/snb-aligned-indexed-20260904 (per-engine r1..r3 receipts + aggregate.json, built with the release lane's own tools); the polars arm's receipts/aggregate come from the release lane tools, the pandas arm's from scripts/aggregate_snb_arm_runs.py (the release receipt contract accepts only the Polars arm)", + "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", + "competitor_version": "n/a", + "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board's parameters)", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-04", + "perf_lock_held": true, + "pyg_bench_commit": "1ee6135", + "pygraphistry_commit": "3fb216dd", + "quiet_host": true, + "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", + "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", + "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" + }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", "competitor": "Kuzu, Neo4j, Memgraph", diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 6976996455..1d61aff56b 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -79,19 +79,24 @@ workload, run on the SF0.1 and SF1 datasets without the official LDBC driver. Th internal evidence, not an official LDBC result. All four engines ran under one timing contract with exact result parity. Times are milliseconds. -Kuzu, Neo4j, and Memgraph are all faster than GFQL on every universal cell, and -Memgraph is fastest in every row. These are point lookups and small results, where a -database's index and per-call floor beat GFQL's per-call compile and row pipeline. -GFQL's strengths are the bulk shapes above and on the :doc:`speedup case study -`; choose a database when the workload is dominated by -point lookups. +Kuzu, Neo4j, and Memgraph are faster than GFQL on every point-lookup row, and Memgraph +is fastest on most. The GFQL columns run with resident indexes built once before the +timed runs (``gfql_index_all`` plus node property indexes), the same footing as the +databases' primary-key and label indexes. The index engages on the hop-shaped rows +(message replies, recent replies) and the cost there drops by 2.5x to 3x; on the pure +point lookups it does not engage, and what remains is a fixed per-call cost in the +chain pipeline of about 20 ms on the pandas and polars engines +(`#2027 `_) against a database's +sub-millisecond index probe. GFQL's strengths are the bulk shapes above and on the +:doc:`speedup case study `; choose a database when the +workload is dominated by point lookups. SF0.1 ~~~~~ .. bench-board:: snb.sf01 :rows: seed_lookup,message_content,message_creator,recent_replies,message_replies,new_topics - :columns: gfql_polars=GFQL polars, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph + :columns: gfql_polars_idx=GFQL polars, gfql_pandas_idx=GFQL pandas, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph :row-labels: seed_lookup=seed lookup; message_content=message content; message_creator=message creator; recent_replies=recent replies; message_replies=message replies (GFQL and Kuzu only); new_topics=new topics (GFQL and Kuzu only) SF1 @@ -99,7 +104,7 @@ SF1 .. bench-board:: snb.sf1 :rows: seed_lookup,message_content,message_creator,new_topics - :columns: gfql_polars=GFQL polars, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph + :columns: gfql_polars_idx=GFQL polars, gfql_pandas_idx=GFQL pandas, kuzu=Kuzu, neo4j=Neo4j, memgraph=Memgraph :row-labels: seed_lookup=seed lookup; message_content=message content; message_creator=message creator; recent_replies=recent replies; message_replies=message replies (GFQL and Kuzu only); new_topics=new topics (GFQL and Kuzu only) Neo4j and Memgraph use a reduced adapter for one query, and one parameter returns zero @@ -143,7 +148,7 @@ Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.js which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` reject missing, stale, or unpublished values. -.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 +.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 snb-aligned-indexed-20260904 :disclosures: Next steps From ac315ab91005a6a33bbe0f0206da27cbb00b4393 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 12:33:43 -0700 Subject: [PATCH 19/29] docs(gfql): re-vendor (drift waiver for the competitor-only GPlus Neo4j run) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh7i1 --- docs/source/_data/gfql_benchmarks.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 74a91e9eb4..fed2b0753a 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -3974,11 +3974,12 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T19:24:23Z", - "generated_by": "graphistry/pyg-bench f6269134 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T19:32:04Z", + "generated_by": "graphistry/pyg-bench 1dd7ed62 scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { + "filter-pagerank-gplus-locked-20260830": "Competitor-only cells: this run publishes the Neo4j + GDS GPlus pipeline time (pagerank.gplus.neo4j_gds), which does not depend on graphistry/compute; the GFQL GPlus arms are re-measured at 0.59.0 in filter-pagerank-059-20260904. Re-measure Neo4j only when its own version or the pipeline contract changes.", "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." }, "literal_allowlist": {}, From 0927c65d0621546d29fc9ae49384be2b2b2cbbbd Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 15:24:08 -0700 Subject: [PATCH 20/29] docs(gfql): GraphFrames page: CPU PageRank rows, Friendster 2-hop, eager-vs-streaming and igraph-conversion answers Re-vendors pyg-bench docs-numbers (batch C/D): CPU PageRank cells for LiveJournal and Orkut (GraphFrames wins; solver share shown), Friendster 2-hop cell, Friendster ceiling text (GPU stops at 1-hop, PageRank does not fit on either path), engine guidance for CPU PageRank (#2032), streaming-vs-eager note; charts regenerated. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh1i1 --- docs/source/_data/gfql_benchmarks.json | 395 ++++++++++++++---- .../_static/graphframes/friendster_tasks.svg | 3 +- .../_static/graphframes/livejournal_tasks.svg | 12 +- .../gfql/_static/graphframes/orkut_tasks.svg | 12 +- docs/source/gfql/benchmark_graphframes.rst | 84 ++-- 5 files changed, 381 insertions(+), 125 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index fed2b0753a..d0d8f36ea2 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -2130,8 +2130,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2147,8 +2149,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2158,14 +2162,35 @@ "value": 6123.4, "workload": "GraphFrames tasks Friendster hop1" }, + "graphframes.friendster.hop2.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 164890.2, + "workload": "GraphFrames tasks Friendster hop2" + }, "graphframes.lj.filter.gfql_polars": { "board_quotable": true, "comparison_allowed": true, "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2181,8 +2206,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2199,8 +2226,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2221,8 +2250,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2243,8 +2274,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2260,8 +2293,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2277,8 +2312,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2295,8 +2332,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2317,8 +2356,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2339,8 +2380,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2356,8 +2399,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2373,8 +2418,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2391,8 +2438,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2413,8 +2462,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2435,8 +2486,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2446,14 +2499,35 @@ "value": 3475.8, "workload": "GraphFrames tasks LiveJournal hop2" }, + "graphframes.lj.pagerank.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 52622.4, + "workload": "GraphFrames tasks LiveJournal pagerank" + }, "graphframes.lj.pagerank.gfql_polars_gpu": { "board_quotable": true, "comparison_allowed": true, "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2469,8 +2543,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.lj.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -2488,8 +2564,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2503,6 +2581,50 @@ "value": 18.3, "workload": "GraphFrames tasks LiveJournal pagerank \u2014 gfql-polars-gpu vs graphframes" }, + "graphframes.lj.pagerank.gfql_polars_kernel": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Component of graphframes.lj.pagerank.gfql_polars: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 2834.8, + "workload": "GraphFrames tasks LiveJournal pagerank \u2014 gfql-polars kernel only" + }, + "graphframes.lj.pagerank.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.lj.pagerank.gfql_polars", + "graphframes.lj.pagerank.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.3, + "workload": "GraphFrames tasks LiveJournal pagerank \u2014 gfql-polars vs graphframes" + }, "graphframes.lj.pagerank.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2510,8 +2632,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2527,8 +2651,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2544,8 +2670,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2562,8 +2690,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2584,8 +2714,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2606,8 +2738,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2623,8 +2757,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2640,8 +2776,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2658,8 +2796,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2680,8 +2820,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2702,8 +2844,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2719,8 +2863,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2736,8 +2882,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2754,8 +2902,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2776,8 +2926,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2798,8 +2950,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2809,14 +2963,35 @@ "value": 10885.8, "workload": "GraphFrames tasks Orkut hop2" }, + "graphframes.orkut.pagerank.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 170865.5, + "workload": "GraphFrames tasks Orkut pagerank" + }, "graphframes.orkut.pagerank.gfql_polars_gpu": { "board_quotable": true, "comparison_allowed": true, "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2832,8 +3007,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.orkut.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -2851,8 +3028,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2866,6 +3045,50 @@ "value": 12.5, "workload": "GraphFrames tasks Orkut pagerank \u2014 gfql-polars-gpu vs graphframes" }, + "graphframes.orkut.pagerank.gfql_polars_kernel": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Component of graphframes.orkut.pagerank.gfql_polars: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "value": 7834.5, + "workload": "GraphFrames tasks Orkut pagerank \u2014 gfql-polars kernel only" + }, + "graphframes.orkut.pagerank.gfql_polars_vs_graphframes": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "graphframes", + "decimals": 1, + "disclosures": [ + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + ], + "engine": "gfql-polars", + "measurement_profile": "graphframes-tasks-warm-resident", + "operands": [ + "graphframes.orkut.pagerank.gfql_polars", + "graphframes.orkut.pagerank.graphframes" + ], + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "x", + "value": 0.2, + "workload": "GraphFrames tasks Orkut pagerank \u2014 gfql-polars vs graphframes" + }, "graphframes.orkut.pagerank.graphframes": { "board_quotable": true, "comparison_allowed": true, @@ -2873,8 +3096,10 @@ "decimals": 1, "disclosures": [ "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", - "GFQL filter and hop rows were measured at the head of pull request #2024 (commit e951e9a), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd).", - "Friendster (1.8B edges) ran filter and 1-hop only, on the Polars CPU streaming lane: peak resident memory reached 106 GB of the 119 GB host after 1-hop, and the Orkut receipt (17 GB peak on 2-hop for 117M edges) projects 2-hop past the host, so 2-hop and PageRank were not attempted; GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", + "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -3974,8 +4199,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T19:32:04Z", - "generated_by": "graphistry/pyg-bench 1dd7ed62 scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T22:21:19Z", + "generated_by": "graphistry/pyg-bench c8e8125b scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4076,7 +4301,7 @@ "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars-igraph (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2, python-igraph 1.0.0) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "graphframes-ladder-20260904": { "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", @@ -4087,11 +4312,11 @@ "measured_at": "2026-09-04", "perf_lock_held": true, "pyg_bench_commit": "0f8e213", - "pygraphistry_commit": "e951e9a", + "pygraphistry_commit": "e951e9a2c", "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars-igraph (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2, python-igraph 1.0.0) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "snb-aligned-indexed-20260904": { "artifact": "results/snb-aligned-indexed-20260904 (per-engine r1..r3 receipts + aggregate.json, built with the release lane's own tools); the polars arm's receipts/aggregate come from the release lane tools, the pandas arm's from scripts/aggregate_snb_arm_runs.py (the release receipt contract accepts only the Polars arm)", diff --git a/docs/source/gfql/_static/graphframes/friendster_tasks.svg b/docs/source/gfql/_static/graphframes/friendster_tasks.svg index 26dad4e7dc..52fee27401 100644 --- a/docs/source/gfql/_static/graphframes/friendster_tasks.svg +++ b/docs/source/gfql/_static/graphframes/friendster_tasks.svg @@ -26,7 +26,8 @@ 2-hop from 50 seeds GFQL polars (CPU) -not measured + +164890.2 ms GFQL polars-gpu (GPU) not measured GraphFrames local[*] diff --git a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg index e2ba55f30e..da91eced7e 100644 --- a/docs/source/gfql/_static/graphframes/livejournal_tasks.svg +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -41,13 +41,15 @@ PageRank, full graph GFQL polars (CPU) -not measured + + +52622.4 ms(solver 2834.8 ms)0.3x of GraphFrames’ speed (slower) GFQL polars-gpu (GPU) - + -890.9 ms(solver 59.3 ms)18.3x faster than GraphFrames +890.9 ms(solver 59.3 ms)18.3x faster than GraphFrames GraphFrames local[*] - -16311.2 ms + +16311.2 ms Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. diff --git a/docs/source/gfql/_static/graphframes/orkut_tasks.svg b/docs/source/gfql/_static/graphframes/orkut_tasks.svg index fecf884474..89dadca9e6 100644 --- a/docs/source/gfql/_static/graphframes/orkut_tasks.svg +++ b/docs/source/gfql/_static/graphframes/orkut_tasks.svg @@ -41,13 +41,15 @@ PageRank, full graph GFQL polars (CPU) -not measured + + +170865.5 ms(solver 7834.5 ms)0.2x of GraphFrames’ speed (slower) GFQL polars-gpu (GPU) - + -2795.7 ms(solver 151.4 ms)12.5x faster than GraphFrames +2795.7 ms(solver 151.4 ms)12.5x faster than GraphFrames GraphFrames local[*] - -34860.7 ms + +34860.7 ms Bars are scaled per task; on PageRank the solid part is the solver alone, the light part the rest of the query. Single node; Spark local[*]. diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 231bb21334..33b92415c4 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -17,16 +17,22 @@ commits. **Where it stands.** The single-server ceiling measured here is Friendster: 1,806,067,135 edges bound from a lazy Polars scan, a degree filter in -:bench:`graphframes.friendster.filter.gfql_polars` and a 1-hop from 50 hub seeds in -:bench:`graphframes.friendster.hop1.gfql_polars` on the CPU streaming path, with resident -memory peaking at 106 GB of the 119 GB host; a 2-hop does not fit, and GraphFrames on -``local[*]`` did not load the graph at all. Below that ceiling the picture is mixed and -both sides are printed: on whole-graph PageRank GFQL on the GPU is +:bench:`graphframes.friendster.filter.gfql_polars`, a 1-hop from 50 hub seeds in +:bench:`graphframes.friendster.hop1.gfql_polars`, and a 2-hop in +:bench:`graphframes.friendster.hop2.gfql_polars` on the CPU streaming path, with resident +memory peaking at 106 GB of the 119 GB host; the GPU path stops at the 1-hop, PageRank +does not fit on either path, and GraphFrames on ``local[*]`` did not load the graph at +all. Below that ceiling the picture is mixed and both sides are printed: on whole-graph +PageRank GFQL on the GPU is :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` faster than GraphFrames on LiveJournal and :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` on -Orkut, and the solver is a small part of that time (the shaded bars); on degree filters -and 1-hop the CPU engine is faster on both graphs; on 2-hop GraphFrames wins on both -(:bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` and +Orkut, while GFQL on the CPU loses PageRank on both +(:bench:`graphframes.lj.pagerank.gfql_polars_vs_graphframes` and +:bench:`graphframes.orkut.pagerank.gfql_polars_vs_graphframes` of GraphFrames' speed): +on both paths the solver is a small part of the time (the shaded bars) and the rest is +the conversion into the solver's graph and the join of scores back onto the nodes; on +degree filters and 1-hop the CPU engine is faster on both graphs; on 2-hop GraphFrames +wins on both (:bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` and :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` of its speed). The GFQL filter and hop rows were measured at the head of the fix for `#2023 `_ @@ -41,14 +47,17 @@ measured); the released code's LiveJournal 2-hop was :alt: Orkut task times: GFQL and GraphFrames for filter, 1-hop, 2-hop, and PageRank, with the PageRank solver time shaded inside the GFQL bar .. image:: _static/graphframes/friendster_tasks.svg - :alt: Friendster task times: GFQL CPU streaming filter and 1-hop; 2-hop, PageRank, and GraphFrames not measured + :alt: Friendster task times: GFQL CPU streaming filter, 1-hop, and 2-hop; PageRank and GraphFrames not measured GFQL binds each graph from a lazy Polars scan of the edge parquet and runs the filter and hop tasks with ``engine="polars"`` under the Polars CPU streaming collect, or with ``engine="polars-gpu"`` under the cudf-polars streaming executor. PageRank re-binds an -eager cuDF copy outside the timer and calls cuGraph. Every cell is the median of 5 timed -runs after 2 warmups, and every task returns the same result size on every system that -ran it. Times are milliseconds unless marked; lower is better. +eager copy outside the timer and calls cuGraph on the GPU or igraph on the CPU. The +streaming collect is not a tax: with the same commit and protocol the eager collect +matched it on filter and 2-hop and was slower on 1-hop (the receipts are named in the +Measurement block). Every cell is the median of 5 timed runs after 2 warmups, and every +task returns the same result size on every system that ran it. Times are milliseconds +unless marked; lower is better. LiveJournal ----------- @@ -78,10 +87,10 @@ LiveJournal - :bench:`graphframes.lj.hop2.graphframes` - :bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - - not measured (CPU PageRank routes through igraph) + - :bench:`graphframes.lj.pagerank.gfql_polars`; solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_kernel` - :bench:`graphframes.lj.pagerank.gfql_polars_gpu`; solver :bench-diag:`graphframes.lj.pagerank.gfql_polars_gpu_kernel` - :bench:`graphframes.lj.pagerank.graphframes` - - GPU: :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes` + - GPU: :bench:`graphframes.lj.pagerank.gfql_polars_gpu_vs_graphframes`; CPU: :bench:`graphframes.lj.pagerank.gfql_polars_vs_graphframes` (GraphFrames wins) The GPU streaming executor is slower than the CPU streaming collect on both hops here (:bench:`graphframes.lj.hop1.gfql_polars_gpu` against @@ -116,15 +125,16 @@ Orkut - :bench:`graphframes.orkut.hop2.graphframes` - :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - - not measured + - :bench:`graphframes.orkut.pagerank.gfql_polars`; solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_kernel` - :bench:`graphframes.orkut.pagerank.gfql_polars_gpu`; solver :bench-diag:`graphframes.orkut.pagerank.gfql_polars_gpu_kernel` - :bench:`graphframes.orkut.pagerank.graphframes` - - GPU: :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes` + - GPU: :bench:`graphframes.orkut.pagerank.gfql_polars_gpu_vs_graphframes`; CPU: :bench:`graphframes.orkut.pagerank.gfql_polars_vs_graphframes` (GraphFrames wins) Friendster ---------- -One attempt, on the Polars CPU streaming path; no other system ran. +On the Polars CPU streaming path; the GPU path stopped at the 1-hop and no other system +ran (see :ref:`graphframes-friendster`). .. list-table:: :header-rows: 1 @@ -139,7 +149,10 @@ One attempt, on the Polars CPU streaming path; no other system ran. * - **1-hop** (50 seeds) - :bench:`graphframes.friendster.hop1.gfql_polars` - 166,615 nodes - * - **2-hop**, **PageRank** + * - **2-hop** (50 seeds) + - :bench:`graphframes.friendster.hop2.gfql_polars` + - 15,878,312 nodes + * - **PageRank** - not attempted - see :ref:`graphframes-friendster` @@ -167,7 +180,7 @@ Result sizes agree across the systems that ran each task, as recorded in the rec * - Friendster (GFQL only) - 6,585,312 - 166,615 - - not attempted + - 15,878,312 - not attempted Which engine to use @@ -182,8 +195,13 @@ Which engine to use - **2-hop from hub seeds**: GraphFrames wins on both graphs today. GFQL's cost is the wavefront seed-rediscovery rule evaluated over the traversed ball; #2024 removed the interpreter loop, and the remaining gap is the rule itself. -- **PageRank without a GPU**: GFQL routes the CPU path through single-threaded igraph. - It is not measured on this page; use it for convenience, not for speed. +- **PageRank without a GPU**: GFQL routes the CPU path through igraph, and loses to + GraphFrames on both graphs. The igraph solver itself is + :bench-diag:`graphframes.lj.pagerank.gfql_polars_kernel` of the + :bench:`graphframes.lj.pagerank.gfql_polars` LiveJournal row; the rest is the + conversion into igraph and the join-back, tracked in + `#2032 `_. Use the CPU path + for convenience, not for speed. - **Larger than one node's memory**: see :ref:`graphframes-friendster`. The tasks @@ -221,10 +239,12 @@ and ``find`` matches a fixed motif. The Spark side therefore expands with one it undirected edge join per hop and ends in ``.count()``. **PageRank**: full graph, damping 0.85. GFQL GPU calls -``g.compute_cugraph('pagerank')`` on an eager cuDF copy of the graph. GraphFrames calls -``gf.pageRank(resetProbability=0.15, maxIter=20)``. Both return the full vertex set. The -shaded part of a GFQL PageRank bar is the cuGraph solver alone on a graph object built -outside the timer; the light part is the rest of the query. +``g.compute_cugraph('pagerank')`` on an eager cuDF copy of the graph; GFQL CPU calls +``g.compute_igraph('pagerank')`` on an eager pandas copy. GraphFrames calls +``gf.pageRank(resetProbability=0.15, maxIter=20)``. All return the full vertex set. The +shaded part of a GFQL PageRank bar is the solver alone (cuGraph or igraph) on a graph +object built outside the timer; the light part is the rest of the query, which is the +conversion into that graph object and the join of scores back onto the nodes. .. _graphframes-friendster: @@ -243,11 +263,17 @@ The harness binds from ``pl.scan_parquet`` and collects through GFQL's streaming peak-memory receipt at every size. On Friendster the CPU streaming run loaded the graph (scan plus degree pass in about 20 seconds, 56 GB resident), answered the degree filter and the 1-hop from 50 hub seeds (table above), and peaked at 106 GB resident after the -1-hop. The Orkut receipt puts 2-hop at 17 GB for 117M edges, and the 2-hop ball from hub -seeds grows with the graph, so 2-hop and PageRank were not attempted: the streaming +1-hop; a second run answered the 2-hop, a 15,878,312-node ball, in +:bench:`graphframes.friendster.hop2.gfql_polars` at 69.5 GB resident. The streaming collect keeps the load out of memory, but the traversal still materializes the edges it -touches. That is the single-server ceiling this page measured. GraphFrames on -``local[*]`` stays at the boundary it hit above. +touches, and that is where the GPU path stops: the cudf-polars streaming executor +completed the degree filter, then the memory guard ended the run during the 1-hop at 106 +GB resident, so the GPU column has no Friendster cell. Whole-graph PageRank does not fit +on either path: the GPU preflight refused it (an estimated 87 GB peak against an 80 GB +budget), and the CPU path was not attempted because its Orkut row already projects the +igraph conversion past the host at fifteen times the edges. That is the single-server +ceiling this page measured. GraphFrames on ``local[*]`` stays at the boundary it hit +above. .. _graphframes-method: From b97015f64ae6bbe9ab4ea806e975562ad87f23b6 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 15:40:19 -0700 Subject: [PATCH 21/29] docs(gfql): GraphFrames page: re-vendor #246 review fixes; memory figures in GiB from the receipts Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh1i1 --- docs/source/_data/gfql_benchmarks.json | 529 +++++++++++++-------- docs/source/gfql/benchmark_graphframes.rst | 17 +- 2 files changed, 344 insertions(+), 202 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index d0d8f36ea2..98af68882a 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -2129,11 +2129,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2148,11 +2151,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2167,11 +2173,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2186,11 +2195,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2205,11 +2217,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2225,11 +2240,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2249,11 +2267,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2273,11 +2294,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2292,11 +2316,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2311,11 +2338,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2331,11 +2361,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2355,11 +2388,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2379,11 +2415,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2398,11 +2437,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2417,11 +2459,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2437,11 +2482,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2461,11 +2509,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2485,11 +2536,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2504,11 +2558,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2523,11 +2580,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2542,11 +2602,14 @@ "comparison_allowed": false, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.lj.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -2563,11 +2626,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2586,11 +2652,14 @@ "comparison_allowed": false, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.lj.pagerank.gfql_polars: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars", @@ -2607,11 +2676,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2631,11 +2703,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2650,11 +2725,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2669,11 +2747,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2689,11 +2770,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2713,11 +2797,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2737,11 +2824,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2756,11 +2846,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2775,11 +2868,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2795,11 +2891,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2819,11 +2918,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2843,11 +2945,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2862,11 +2967,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2881,11 +2989,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2901,11 +3012,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2925,11 +3039,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2949,11 +3066,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2968,11 +3088,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -2987,11 +3110,14 @@ "comparison_allowed": true, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -3006,11 +3132,14 @@ "comparison_allowed": false, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.orkut.pagerank.gfql_polars_gpu: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars-gpu", @@ -3027,11 +3156,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars-gpu", "measurement_profile": "graphframes-tasks-warm-resident", @@ -3050,11 +3182,14 @@ "comparison_allowed": false, "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison.", "Component of graphframes.orkut.pagerank.gfql_polars: the PageRank solver alone on a graph object built outside the timer; the parent time adds the conversion and the join of scores back onto the nodes." ], "engine": "gfql-polars", @@ -3071,11 +3206,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "gfql-polars", "measurement_profile": "graphframes-tasks-warm-resident", @@ -3095,11 +3233,14 @@ "competitor": "graphframes", "decimals": 1, "disclosures": [ - "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF copy outside the timer.", + "Single node (dgx-spark, NVIDIA GB10 unified memory, 20 CPU); GraphFrames on Spark local[*] over all cores with a 90g driver, PySpark 3.5.3; GFQL binds the graph from a lazy Polars scan and its filter and hop rows run under the Polars CPU streaming collect or the cudf-polars streaming executor; PageRank rows re-bind an eager cuDF (GPU) or pandas (CPU) copy outside the timer.", "GFQL filter, hop and CPU PageRank rows were measured at the head of pull request #2024 (commit e951e9a2c), the fix for issue #2023 (a per-edge Python loop that made undirected multi-hop ~30x slower at 0.59.0); it had not landed on master when measured. GFQL GPU PageRank rows and the GraphFrames rows were measured at 0.59.0 (3fb216dd); the PageRank code is the same at both commits.", - "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 8% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms), so the published rows are the streaming lane and there is no streaming tax to disclose.", - "CPU PageRank runs the igraph solver: on LiveJournal the solver is 2.8 s of the 52.6 s row and on Orkut 7.8 s of 170.9 s; the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032), which is why the CPU rows trail GraphFrames while the GPU rows lead.", - "Friendster (1.8B edges): filter and 1-hop ran on the Polars CPU streaming lane (peak resident 106 GB of the 119 GB host after 1-hop); 2-hop ran on the same lane without the address-space cap (164.9 s, 15.9M-node ball, 69.5 GB peak resident). The GPU lane completed the filter (4.0 s, diagnostic only) and was then stopped by the memory guard during 1-hop at 106 GB resident, and GPU PageRank was refused by preflight (estimated 87 GB peak over the 80 GB budget); CPU PageRank was not attempted because the Orkut row projects the igraph conversion past the host at 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." + "Polars CPU streaming versus eager collect (receipts fix2024/lj-polars and fix2024/orkut-polars, same commit and protocol): filter and 2-hop medians agree within 9% (LiveJournal 2-hop 6.5 s streaming vs 7.0 s eager; Orkut 27.3 s vs 27.3 s) and streaming is faster on 1-hop (LiveJournal 220 ms vs 336 ms; Orkut 514 ms vs 1,031 ms); the published rows are the streaming lane.", + "CPU PageRank runs the igraph solver: on LiveJournal the solver alone is 2.8 s of the 52.6 s row (the GraphFrames row is 16.3 s) and on Orkut 7.8 s of 170.9 s (GraphFrames 34.9 s); the rest is the frame-to-igraph conversion and the join-back of scores (pygraphistry issue #2032).", + "Friendster (1.8B edges) filter and 1-hop ran on the Polars CPU streaming lane; resident memory after the 1-hop was 106,095 MiB (103.6 GiB) of the 119 GiB host.", + "Friendster 2-hop ran on the same lane without the host address-space cap: 164.9 s, a 15,878,312-node ball, 69,526 MiB (67.9 GiB) resident. The first attempt under a 100 GB address-space cap (fix2024/friendster-polars-hop2.failed-attempt1) ended at load with a failed 7.2 GB allocation, exit 133.", + "Friendster on the cudf-polars streaming executor (fix2024/friendster-polars-gpu-streaming.killed-attempt1): the degree filter completed (4.0 s, a diagnostic from a killed lane, not a cell) with the process at 106,308 MiB resident; the watchdog killed the run during the 1-hop when host free memory fell to 17 GB against the 20 GB floor, exit 137. The GPU column has no Friendster cell.", + "Friendster GPU PageRank was refused by preflight (fix2024/friendster-polars-gpu-pagerank.refused-attempt1: estimated 87 GB peak against the 80 GB budget). CPU PageRank was not attempted: Orkut's CPU PageRank peaked at 30,586 MiB resident at 117M edges and Friendster has 15x the edges. GraphFrames on local[*] did not run on Friendster (a 90 GB driver heap swapped in the earlier eager attempt). Friendster rows are direct times with no comparison." ], "engine": "graphframes", "measurement_profile": "graphframes-tasks-warm-resident", @@ -4199,8 +4340,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T22:21:19Z", - "generated_by": "graphistry/pyg-bench c8e8125b scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T22:38:33Z", + "generated_by": "graphistry/pyg-bench e757e71a scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4301,22 +4442,22 @@ "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars-igraph (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2, python-igraph 1.0.0) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "graphframes-ladder-20260904": { - "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop rungs; stale-attempts/ the breached ones)", + "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop, eager and CPU PageRank rungs; stale-attempts/ the breached ones; fix2024/*.killed-attempt1, *.failed-attempt1 and *.refused-attempt1 the Friendster runs that did not complete, with their launch logs)", "competitor": "Apache Spark GraphFrames", "competitor_version": "graphframes assembly jar 1a4826bc (sha256 in each rung's SOURCE_COMMITS) on PySpark 3.5.3, OpenJDK 1.8.0_492, Spark local[*]", "dataset": "SNAP com-LiveJournal (3,997,962 nodes / 34,681,189 edges), com-Orkut (3,072,441 nodes / 117,185,083 edges), com-Friendster (65,608,366 nodes / 1,806,067,135 edges) as parquet edge lists; 50 highest-degree seeds; filter thresholds 42 / 162 / 148 (Friendster: the 90th degree percentile)", "host": "dgx-spark (NVIDIA GB10), 20 CPU", "measured_at": "2026-09-04", "perf_lock_held": true, - "pyg_bench_commit": "0f8e213", + "pyg_bench_commit": "f626913 (fix2024 rungs; the runner gained the optional HOST_GB address-space cap after 0f8e213, the bench script is unchanged, sha256 in each rung's runner-sha256.txt)", "pygraphistry_commit": "e951e9a2c", "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars-igraph (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2, python-igraph 1.0.0) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.35.2, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); the two CPU PageRank rungs (fix2024/lj-polars-pagerank, orkut-polars-pagerank) in graphistry/test-rapids-official:26.02-gfql-polars-igraph, the same image plus python-igraph 1.0.0 (image id and versions in each rung's IMAGE_RECEIPT.txt, recorded after the run); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "snb-aligned-indexed-20260904": { "artifact": "results/snb-aligned-indexed-20260904 (per-engine r1..r3 receipts + aggregate.json, built with the release lane's own tools); the polars arm's receipts/aggregate come from the release lane tools, the pandas arm's from scripts/aggregate_snb_arm_runs.py (the release receipt contract accepts only the Polars arm)", diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 33b92415c4..9de701afa7 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -20,7 +20,7 @@ commits. :bench:`graphframes.friendster.filter.gfql_polars`, a 1-hop from 50 hub seeds in :bench:`graphframes.friendster.hop1.gfql_polars`, and a 2-hop in :bench:`graphframes.friendster.hop2.gfql_polars` on the CPU streaming path, with resident -memory peaking at 106 GB of the 119 GB host; the GPU path stops at the 1-hop, PageRank +memory peaking at 103.6 GiB of the 119 GiB host; the GPU path stops at the 1-hop, PageRank does not fit on either path, and GraphFrames on ``local[*]`` did not load the graph at all. Below that ceiling the picture is mixed and both sides are printed: on whole-graph PageRank GFQL on the GPU is @@ -261,17 +261,18 @@ The harness binds from ``pl.scan_parquet`` and collects through GFQL's streaming (``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, ``GFQL_POLARS_GPU_EXECUTOR=streaming`` for the cudf-polars streaming executor), with a peak-memory receipt at every size. On Friendster the CPU streaming run loaded the graph -(scan plus degree pass in about 20 seconds, 56 GB resident), answered the degree filter -and the 1-hop from 50 hub seeds (table above), and peaked at 106 GB resident after the +(scan plus degree pass in about 20 seconds, 55.0 GiB resident), answered the degree filter +and the 1-hop from 50 hub seeds (table above), and peaked at 103.6 GiB resident after the 1-hop; a second run answered the 2-hop, a 15,878,312-node ball, in -:bench:`graphframes.friendster.hop2.gfql_polars` at 69.5 GB resident. The streaming +:bench:`graphframes.friendster.hop2.gfql_polars` at 67.9 GiB resident. The streaming collect keeps the load out of memory, but the traversal still materializes the edges it touches, and that is where the GPU path stops: the cudf-polars streaming executor -completed the degree filter, then the memory guard ended the run during the 1-hop at 106 -GB resident, so the GPU column has no Friendster cell. Whole-graph PageRank does not fit +completed the degree filter at 103.8 GiB resident, then the watchdog ended the run during +the 1-hop when host free memory fell to 17 GB against its 20 GB floor, so the GPU column +has no Friendster cell. Whole-graph PageRank does not fit on either path: the GPU preflight refused it (an estimated 87 GB peak against an 80 GB -budget), and the CPU path was not attempted because its Orkut row already projects the -igraph conversion past the host at fifteen times the edges. That is the single-server +budget), and the CPU path was not attempted: its Orkut row peaked at 29.9 GiB resident +for 117M edges, and Friendster has fifteen times the edges. That is the single-server ceiling this page measured. GraphFrames on ``local[*]`` stays at the boundary it hit above. From 65cff506ab7d257d8071f0f255d3007bc4f4ab4e Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 15:44:37 -0700 Subject: [PATCH 22/29] docs(gfql): re-vendor (diagnostic run's runtime names polars 1.35.2) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh1i1 --- docs/source/_data/gfql_benchmarks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 98af68882a..6c01a62ac2 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -4340,8 +4340,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T22:38:33Z", - "generated_by": "graphistry/pyg-bench e757e71a scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T22:44:34Z", + "generated_by": "graphistry/pyg-bench 6ed12f1b scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4442,7 +4442,7 @@ "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", - "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.42, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rung; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" + "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.35.2, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "graphframes-ladder-20260904": { "artifact": "results/graphframes-ladder-20260904 (one receipted rung directory per dataset x system x task group; fix2024/ holds the #2024-head hop, eager and CPU PageRank rungs; stale-attempts/ the breached ones; fix2024/*.killed-attempt1, *.failed-attempt1 and *.refused-attempt1 the Friendster runs that did not complete, with their launch logs)", From c315e96ace3183672bcc1a6d95da298f0dd1fe25 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 15:49:32 -0700 Subject: [PATCH 23/29] docs(gfql): re-vendor (per-rung pyg-bench commit clause) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh1i1 --- docs/source/_data/gfql_benchmarks.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 6c01a62ac2..486f22b616 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -4340,8 +4340,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T22:44:34Z", - "generated_by": "graphistry/pyg-bench 6ed12f1b scripts/export_docs_numbers.py", + "generated_at": "2026-09-04T22:49:12Z", + "generated_by": "graphistry/pyg-bench d70caf55 scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4452,7 +4452,7 @@ "host": "dgx-spark (NVIDIA GB10), 20 CPU", "measured_at": "2026-09-04", "perf_lock_held": true, - "pyg_bench_commit": "f626913 (fix2024 rungs; the runner gained the optional HOST_GB address-space cap after 0f8e213, the bench script is unchanged, sha256 in each rung's runner-sha256.txt)", + "pyg_bench_commit": "f626913 for the batch C/D rungs (fix2024/lj-polars, orkut-polars, lj-polars-pagerank, orkut-polars-pagerank, friendster-polars-hop2-nocap); the earlier fix2024 rungs record 9757665 and the 0.59 rungs 0f8e213 in their SOURCE_COMMITS; the runner gained the optional HOST_GB address-space cap after 0f8e213 and the bench script is unchanged (sha256 in each rung's runner-sha256.txt)", "pygraphistry_commit": "e951e9a2c", "quiet_host": true, "reps": "each system loads once and stays resident; 2 warmups + 5 timed runs per task, median; GraphFrames PageRank 20 iterations", From f13184d489ab2bbd894a1a4167ab1a3855108e4e Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 16:19:24 -0700 Subject: [PATCH 24/29] changelog: release-notes entry for the GFQL latency regressions fixed and the gates added Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WwMmVFo44ADiRRj5cxh1i1 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1529ef4f53..d74a5fdcb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * GFQL: the general chain path keeps integer and boolean node attribute dtypes on a closed graph: the endpoint-closure backfill now appends only endpoints missing from the node frame instead of concatenating every endpoint id and deduplicating afterwards, which widened the attribute columns to float on every route that fell through the hot paths (#2058, chain seam; the row-pipeline pivot dtypes are a separate item). * GFQL: the chain specializations move into `graphistry/compute/chain_specializations/{admission,hotpaths}.py` (pandas/cuDF single-node lane, seeded typed single hop, seeded typed RETURN-destination) and `graphistry/compute/gfql/lazy/engine/polars/chain_specializations/{admission,hotpaths}.py` (polars plain single-hop branches, seeded lane, RETURN-destination), each lane next to the admission predicate the dispatcher calls (`native_fast_path_admits`, `polars_plain_single_hop_admits`, `polars_seeded_lane_admits`); `chain.py` and the polars chain only dispatch, `chain_fast_paths.py` keeps the shared seed/index helpers. No route admits or declines anything it did not before. Tests mirror the new paths and filter one shared shape corpus per route with the route's own gate; `GFQL_ROUTES_OFF=` (test conftest) makes named hot paths decline so every existing test replays through the other routes, and `bin/test-routes-off.sh` reports the per-route divergences. * GFQL: the wavefront seed-rediscovery rule moved out of `hop.py` into `graphistry/compute/gfql/seed_rediscovery.py` (pandas/cuDF) and `graphistry/compute/gfql/lazy/engine/polars/seed_rediscovery.py` (polars); `undirected_rediscovered_seed_ids` (an internal helper) is gone. +### Performance + +* GFQL latency: this release closes a set of point-lookup regressions found while re-measuring the LDBC SNB Interactive shapes, and adds the gates that stop them coming back. Regressions fixed: every Cypher string query on a wide pandas node table re-scanned the object columns to key the compile cache (#2029); an undirected multi-hop paid a per-edge Python loop in the seed-rediscovery rule (#2023) and a whole-frame dedup of the doubled edge frame on pandas and cuDF; the LDBC IS1 form (a seeded hop returning properties of both aliases) and the seeded single-node lookup ran the full chain with no index consulted. Gates added: `test_gfql_latency_contract.py` (fast path served plus a bounded ratio to plain frame ops for the basic shapes on pandas, polars and cuDF), `test_hop_scaling_pin.py` (undirected 2-hop cost bounded relative to two plain joins), and, in the private benchmark repository, a point-latency sentinel on the real SF0.1 fixture with absolute sub-10 ms thresholds that runs on every release candidate before the documentation numbers are re-vendored, plus an exporter gate that fails any unexplained slowdown or new loss in the published cells. + ### Docs - **GFQL docs navigation is split into a short start path and two hubs**: `gfql/index` now lists four Start Here pages, a Performance and Benchmarks hub (`gfql/perf/index`), a Language Reference hub (`gfql/reference/index`), and the developer specs. No page moved, so existing URLs are unchanged; the previously unlisted `gfql/loading_graph_data` page joins the reference hub. From c1c5eedb26ffdbd40a9473cbc12c1331b3815083 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Fri, 4 Sep 2026 20:16:05 -0700 Subject: [PATCH 25/29] docs(gfql): SNB section renders the SF0.1 GFQL arms re-measured with index-resolved seeds Vendors pyg-bench published/docs-numbers.json at main 5cf81ae (byte-identical): the SF0.1 indexed GFQL cells come from the #2038 head with the pre-landing disclosure, SF1 stays the release measurement. The SNB prose states the native op-list arm, where the resident indexes now engage, the hop rows they do not change, and the open issues behind the remaining gaps. Release-notes Performance paragraph names the native seed resolution and the sentinel's native op-list points. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014RhboAkmjP5eXbCCkX1BMt --- CHANGELOG.md | 2 +- docs/source/_data/gfql_benchmarks.json | 115 ++++++++++++++++--------- docs/source/gfql/performance.rst | 32 ++++--- 3 files changed, 95 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74a5fdcb2..cd7bdce7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm * GFQL: the wavefront seed-rediscovery rule moved out of `hop.py` into `graphistry/compute/gfql/seed_rediscovery.py` (pandas/cuDF) and `graphistry/compute/gfql/lazy/engine/polars/seed_rediscovery.py` (polars); `undirected_rediscovered_seed_ids` (an internal helper) is gone. ### Performance -* GFQL latency: this release closes a set of point-lookup regressions found while re-measuring the LDBC SNB Interactive shapes, and adds the gates that stop them coming back. Regressions fixed: every Cypher string query on a wide pandas node table re-scanned the object columns to key the compile cache (#2029); an undirected multi-hop paid a per-edge Python loop in the seed-rediscovery rule (#2023) and a whole-frame dedup of the doubled edge frame on pandas and cuDF; the LDBC IS1 form (a seeded hop returning properties of both aliases) and the seeded single-node lookup ran the full chain with no index consulted. Gates added: `test_gfql_latency_contract.py` (fast path served plus a bounded ratio to plain frame ops for the basic shapes on pandas, polars and cuDF), `test_hop_scaling_pin.py` (undirected 2-hop cost bounded relative to two plain joins), and, in the private benchmark repository, a point-latency sentinel on the real SF0.1 fixture with absolute sub-10 ms thresholds that runs on every release candidate before the documentation numbers are re-vendored, plus an exporter gate that fails any unexplained slowdown or new loss in the published cells. +* GFQL latency: this release closes a set of point-lookup regressions found while re-measuring the LDBC SNB Interactive shapes, and adds the gates that stop them coming back. Regressions fixed: every Cypher string query on a wide pandas node table re-scanned the object columns to key the compile cache (#2029); an undirected multi-hop paid a per-edge Python loop in the seed-rediscovery rule (#2023) and a whole-frame dedup of the doubled edge frame on pandas and cuDF; the LDBC IS1 form (a seeded hop returning properties of both aliases) and the seeded single-node lookup ran the full chain with no index consulted; native op-list chains scanned the node table for a seed given on a non-binding property even with a node-property index resident, declined named patterns that the chain fast path could serve, and on polars never consulted the resident seed indexes at all (#2027, #2033; now served through the resident node-id, adjacency and node-property indexes on pandas, cuDF and polars, with the engagement visible in `gfql_explain`). Gates added: `test_gfql_latency_contract.py` (fast path served plus a bounded ratio to plain frame ops for the basic shapes on pandas, polars and cuDF), `test_hop_scaling_pin.py` (undirected 2-hop cost bounded relative to two plain joins), and, in the private benchmark repository, a point-latency sentinel on the real SF0.1 fixture with absolute sub-10 ms thresholds that runs on every release candidate before the documentation numbers are re-vendored, plus an exporter gate that fails any unexplained slowdown or new loss in the published cells. The sentinel also times native op-list twins of the same shapes with untimed receipts of the route that served them and value parity with the full path, so a fast path that stops engaging is reported rather than hidden behind a still-correct answer. ### Docs diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 486f22b616..d5e39e3008 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -3447,14 +3447,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 10.059, + "value": 1.686, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.gfql_polars": { @@ -3477,14 +3478,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 3.591, + "value": 3.059, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.kuzu": { @@ -3540,14 +3542,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 26.569, + "value": 2.345, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.gfql_polars": { @@ -3570,14 +3573,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 16.078, + "value": 6.812, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.kuzu": { @@ -3633,14 +3637,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 13.847, + "value": 14.538, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.gfql_polars": { @@ -3663,14 +3668,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 14.668, + "value": 12.977, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.kuzu": { @@ -3694,14 +3700,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 70.391, + "value": 73.506, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.gfql_polars": { @@ -3724,14 +3731,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 43.151, + "value": 42.903, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.kuzu": { @@ -3755,14 +3763,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 47.5, + "value": 50.924, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.gfql_polars": { @@ -3785,14 +3794,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 32.129, + "value": 35.658, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.kuzu": { @@ -3848,14 +3858,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 30.118, + "value": 3.908, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.gfql_polars": { @@ -3878,14 +3889,15 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "ok", "unit": "ms", - "value": 21.817, + "value": 10.836, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.kuzu": { @@ -3942,14 +3954,15 @@ "decimals": 3, "disclosures": [ "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run.", "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "result_mismatch", "unit": "ms", - "value": 59.065, + "value": 61.173, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf01.tag_cooccurrence.gfql_polars_idx": { @@ -3958,14 +3971,15 @@ "decimals": 3, "disclosures": [ "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", + "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run.", "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-aligned-indexed-2038-20260905", "status": "result_mismatch", "unit": "ms", - "value": 30.222, + "value": 26.952, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf1.is3.single_query.polars": { @@ -4340,8 +4354,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-04T22:49:12Z", - "generated_by": "graphistry/pyg-bench d70caf55 scripts/export_docs_numbers.py", + "generated_at": "2026-09-05T03:02:45Z", + "generated_by": "graphistry/pyg-bench 2ffd92d9 scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4460,7 +4474,7 @@ "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.35.2, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); the two CPU PageRank rungs (fix2024/lj-polars-pagerank, orkut-polars-pagerank) in graphistry/test-rapids-official:26.02-gfql-polars-igraph, the same image plus python-igraph 1.0.0 (image id and versions in each rung's IMAGE_RECEIPT.txt, recorded after the run); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, "snb-aligned-indexed-20260904": { - "artifact": "results/snb-aligned-indexed-20260904 (per-engine r1..r3 receipts + aggregate.json, built with the release lane's own tools); the polars arm's receipts/aggregate come from the release lane tools, the pandas arm's from scripts/aggregate_snb_arm_runs.py (the release receipt contract accepts only the Polars arm)", + "artifact": "results/snb-aligned-indexed-20260904 (SF1 cells; its SF0.1 cells are superseded by run snb-aligned-indexed-2038-20260905)", "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", "competitor_version": "n/a", "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board's parameters)", @@ -4474,6 +4488,21 @@ "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" }, + "snb-aligned-indexed-2038-20260905": { + "artifact": "results/snb-aligned-indexed-2038-20260905 (per-engine r1..r3 receipts + aggregate.json built with scripts/aggregate_snb_arm_runs.py from the release runner's run directories)", + "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", + "competitor_version": "n/a", + "dataset": "LDBC SNB Interactive SF0.1, derived matched query shapes (the release board's parameters)", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-05", + "perf_lock_held": true, + "pyg_bench_commit": "dad44f5", + "pygraphistry_commit": "cdbcc640a", + "quiet_host": true, + "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", + "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", + "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" + }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", "competitor": "Kuzu, Neo4j, Memgraph", diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 1d61aff56b..e8ce0e6cb6 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -82,15 +82,26 @@ contract with exact result parity. Times are milliseconds. Kuzu, Neo4j, and Memgraph are faster than GFQL on every point-lookup row, and Memgraph is fastest on most. The GFQL columns run with resident indexes built once before the timed runs (``gfql_index_all`` plus node property indexes), the same footing as the -databases' primary-key and label indexes. The index engages on the hop-shaped rows -(message replies, recent replies) and the cost there drops by 2.5x to 3x; on the pure -point lookups it does not engage, and what remains is a fixed per-call cost in the -chain pipeline of about 20 ms on the pandas and polars engines -(`#2027 `_) against a database's -sub-millisecond index probe. GFQL's strengths are the bulk shapes above and on the -:doc:`speedup case study `; choose a database when the +databases' primary-key and label indexes, and the GFQL arm runs native op lists, not +Cypher text. A seeded lookup, a seeded typed hop, and a node-only lookup now resolve +through the resident node-id, adjacency, and node-property indexes on every CPU engine, +so the SF0.1 point rows sit in the low single-digit milliseconds on pandas and under +about ten milliseconds on polars, against a database's sub-millisecond index probe. The +hop-shaped rows (message replies, recent replies, new topics) are unchanged by that work +and remain GFQL's slowest cells here. GFQL's strengths are the bulk shapes above and on +the :doc:`speedup case study `; choose a database when the workload is dominated by point lookups. +Open items behind the remaining gaps: CPU PageRank spends most of its time converting to +igraph (`#2032 `_); a polars +native chain whose edge alias collides with the edge column its own filter uses raises +where pandas serves it (`#2039 `_); +``rows(table=nodes, source=alias)`` multiplies rows for duplicate node ids +(`#2034 `_). The SF0.1 GFQL +cells were measured at the head of the pull request that landed the index-resolved seeds, +before it reached master; the SF1 cells are the release measurement, and the Measurement +block below carries both. + SF0.1 ~~~~~ @@ -128,8 +139,9 @@ time, over columnar frames based on `Apache Arrow `_. fuses the operations into one lazy plan and collects once; cuDF and Polars GPU run the same columnar operations on NVIDIA GPUs. That favors bulk work: multi-join analytics, frontier expansion from many seeds, and full-graph aggregation. It does not favor -single-row point lookups, where the per-call compile and row-pipeline floor dominates -and an indexed database answers in well under a millisecond, as the SNB tables show. +single-row point lookups: the resident indexes bring a seeded lookup to a few +milliseconds, and an indexed database still answers in well under a millisecond, as the +SNB tables show. Start on CPU with no special hardware, and move to a GPU engine by changing one keyword when the graph or result becomes large. The :doc:`speedup case study @@ -148,7 +160,7 @@ Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.js which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` reject missing, stale, or unpublished values. -.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 snb-aligned-indexed-20260904 +.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 snb-aligned-indexed-20260904 snb-aligned-indexed-2038-20260905 :disclosures: Next steps From 60d39f02aeea33943eadb5d7871fdc78e4741620 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Sat, 5 Sep 2026 19:10:05 -0700 Subject: [PATCH 26/29] docs(gfql): benchmark numbers re-vendored from the master re-measure (pyg-bench #249) docs/source/_data/gfql_benchmarks.json = pyg-bench published/docs-numbers.json after the release close-out re-measure at pygraphistry master 5a6586f22 (SNB SF0.1/SF1 arms, q1-q9 20k/100k boards); provenance directive names the master runs; the SNB pre-landing disclosure is gone (both scales are the release measurement) and the GraphFrames ladder note says the fix has since landed without a re-run. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QztW7jYsDd66e8rb8pJNQA --- docs/source/_data/gfql_benchmarks.json | 703 ++++++++++----------- docs/source/gfql/benchmark_graphframes.rst | 4 +- docs/source/gfql/performance.rst | 9 +- 3 files changed, 343 insertions(+), 373 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index d5e39e3008..1ab629130d 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -8,10 +8,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 152.26, + "value": 152.67, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.memgraph": { @@ -22,7 +22,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 655.5, @@ -36,7 +36,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 680.72, @@ -50,10 +50,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 170.68, + "value": 163.57, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars": { @@ -64,10 +64,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 26.27, + "value": 26.38, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars_gpu": { @@ -78,10 +78,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 18.35, + "value": 18.85, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars_gpu_vs_pandas": { @@ -95,10 +95,10 @@ "graphbench.100k.q1.polars_gpu", "graphbench.100k.q1.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 9.3, + "value": 8.7, "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q1.polars_vs_pandas": { @@ -112,10 +112,10 @@ "graphbench.100k.q1.polars", "graphbench.100k.q1.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 6.5, + "value": 6.2, "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q2.kuzu": { @@ -126,10 +126,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 279.55, + "value": 265.01, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.memgraph": { @@ -140,7 +140,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 468.94, @@ -154,7 +154,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 632.7, @@ -168,10 +168,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 180.22, + "value": 168.17, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars": { @@ -182,10 +182,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 39.68, + "value": 39.95, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars_gpu": { @@ -196,10 +196,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 29.02, + "value": 29.25, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars_gpu_vs_pandas": { @@ -213,10 +213,10 @@ "graphbench.100k.q2.polars_gpu", "graphbench.100k.q2.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 6.2, + "value": 5.8, "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q2.polars_vs_pandas": { @@ -230,10 +230,10 @@ "graphbench.100k.q2.polars", "graphbench.100k.q2.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 4.5, + "value": 4.2, "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q3.kuzu": { @@ -244,10 +244,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 34.71, + "value": 34.45, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.memgraph": { @@ -258,7 +258,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 30.17, @@ -272,7 +272,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 51.14, @@ -286,10 +286,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 75.89, + "value": 69.47, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars": { @@ -300,10 +300,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.7, + "value": 9.92, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars_gpu": { @@ -314,10 +314,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.32, + "value": 9.38, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars_gpu_vs_pandas": { @@ -331,10 +331,10 @@ "graphbench.100k.q3.polars_gpu", "graphbench.100k.q3.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 8.1, + "value": 7.4, "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q3.polars_vs_pandas": { @@ -348,10 +348,10 @@ "graphbench.100k.q3.polars", "graphbench.100k.q3.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 7.8, + "value": 7.0, "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q4.kuzu": { @@ -362,10 +362,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 13.41, + "value": 13.69, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.memgraph": { @@ -376,7 +376,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 37.64, @@ -390,7 +390,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 63.76, @@ -404,10 +404,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 68.95, + "value": 62.79, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars": { @@ -418,10 +418,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 8.97, + "value": 8.75, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars_gpu": { @@ -432,10 +432,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.84, + "value": 10.11, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars_gpu_vs_pandas": { @@ -449,10 +449,10 @@ "graphbench.100k.q4.polars_gpu", "graphbench.100k.q4.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 7.0, + "value": 6.2, "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q4.polars_vs_pandas": { @@ -466,10 +466,10 @@ "graphbench.100k.q4.polars", "graphbench.100k.q4.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 7.7, + "value": 7.2, "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q5.kuzu": { @@ -480,10 +480,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 13.18, + "value": 12.79, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.memgraph": { @@ -494,7 +494,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 3.95, @@ -508,7 +508,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 8.96, @@ -522,10 +522,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 409.36, + "value": 400.64, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars": { @@ -536,10 +536,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 12.85, + "value": 11.37, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars_gpu": { @@ -550,10 +550,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 13.74, + "value": 13.52, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars_gpu_vs_pandas": { @@ -567,10 +567,10 @@ "graphbench.100k.q5.polars_gpu", "graphbench.100k.q5.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 29.8, + "value": 29.6, "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q5.polars_vs_pandas": { @@ -584,10 +584,10 @@ "graphbench.100k.q5.polars", "graphbench.100k.q5.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 31.9, + "value": 35.2, "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q6.kuzu": { @@ -598,10 +598,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 24.14, + "value": 21.49, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.memgraph": { @@ -612,7 +612,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 5.14, @@ -626,7 +626,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 22.94, @@ -640,10 +640,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 416.88, + "value": 400.74, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars": { @@ -654,10 +654,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 12.43, + "value": 14.04, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars_gpu": { @@ -668,10 +668,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 14.55, + "value": 12.37, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars_gpu_vs_pandas": { @@ -685,10 +685,10 @@ "graphbench.100k.q6.polars_gpu", "graphbench.100k.q6.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 28.7, + "value": 32.4, "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q6.polars_vs_pandas": { @@ -702,10 +702,10 @@ "graphbench.100k.q6.polars", "graphbench.100k.q6.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 33.5, + "value": 28.5, "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q7.kuzu": { @@ -716,10 +716,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.25, + "value": 9.81, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.memgraph": { @@ -730,7 +730,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 3.74, @@ -744,7 +744,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 140.18, @@ -758,10 +758,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 124.41, + "value": 113.31, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars": { @@ -772,10 +772,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 7.16, + "value": 7.1, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars_gpu": { @@ -786,10 +786,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 14.25, + "value": 14.56, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars_gpu_vs_pandas": { @@ -803,10 +803,10 @@ "graphbench.100k.q7.polars_gpu", "graphbench.100k.q7.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 8.7, + "value": 7.8, "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q7.polars_vs_pandas": { @@ -820,10 +820,10 @@ "graphbench.100k.q7.polars", "graphbench.100k.q7.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 17.4, + "value": 16.0, "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q8.kuzu": { @@ -834,10 +834,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.87, + "value": 9.74, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.memgraph": { @@ -848,7 +848,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 7316.46, @@ -862,7 +862,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 3766.74, @@ -876,10 +876,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 90.33, + "value": 77.01, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars": { @@ -890,10 +890,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 13.76, + "value": 14.01, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars_gpu": { @@ -904,10 +904,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 171.72, + "value": 178.58, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars_gpu_vs_pandas": { @@ -921,10 +921,10 @@ "graphbench.100k.q8.polars_gpu", "graphbench.100k.q8.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 0.5, + "value": 0.4, "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q8.polars_vs_pandas": { @@ -938,10 +938,10 @@ "graphbench.100k.q8.polars", "graphbench.100k.q8.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 6.6, + "value": 5.5, "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q9.kuzu": { @@ -952,10 +952,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 83.13, + "value": 82.85, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.memgraph": { @@ -966,7 +966,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 6199.8, @@ -980,7 +980,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", "value": 4843.46, @@ -994,10 +994,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 167.96, + "value": 139.95, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars": { @@ -1008,10 +1008,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 34.2, + "value": 34.26, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars_gpu": { @@ -1022,10 +1022,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "ms", - "value": 23.71, + "value": 23.64, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars_gpu_vs_pandas": { @@ -1039,10 +1039,10 @@ "graphbench.100k.q9.polars_gpu", "graphbench.100k.q9.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 7.1, + "value": 5.9, "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q9.polars_vs_pandas": { @@ -1056,10 +1056,10 @@ "graphbench.100k.q9.polars", "graphbench.100k.q9.pandas" ], - "run": "graphbench-q1q9-100k-20260904", + "run": "graphbench-q1q9-100k-master-20260905", "status": "ok", "unit": "x", - "value": 4.9, + "value": 4.1, "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q1.kuzu": { @@ -1070,10 +1070,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 15.32, + "value": 15.37, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.memgraph": { @@ -1084,7 +1084,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 66.08, @@ -1098,7 +1098,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 74.5, @@ -1112,10 +1112,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 30.06, + "value": 27.85, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars": { @@ -1126,10 +1126,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 7.54, + "value": 7.29, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars_gpu": { @@ -1140,10 +1140,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 6.37, + "value": 6.21, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars_gpu_vs_pandas": { @@ -1157,10 +1157,10 @@ "graphbench.20k.q1.polars_gpu", "graphbench.20k.q1.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 4.7, + "value": 4.5, "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q1.polars_vs_pandas": { @@ -1174,10 +1174,10 @@ "graphbench.20k.q1.polars", "graphbench.20k.q1.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 4.0, + "value": 3.8, "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q2.kuzu": { @@ -1188,10 +1188,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 36.01, + "value": 36.4, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.memgraph": { @@ -1202,7 +1202,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 44.41, @@ -1216,7 +1216,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 67.74, @@ -1230,10 +1230,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 32.83, + "value": 29.71, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars": { @@ -1244,10 +1244,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 11.79, + "value": 11.3, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars_gpu": { @@ -1258,10 +1258,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 10.26, + "value": 10.59, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars_gpu_vs_pandas": { @@ -1275,10 +1275,10 @@ "graphbench.20k.q2.polars_gpu", "graphbench.20k.q2.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 3.2, + "value": 2.8, "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q2.polars_vs_pandas": { @@ -1292,10 +1292,10 @@ "graphbench.20k.q2.polars", "graphbench.20k.q2.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.8, + "value": 2.6, "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q3.kuzu": { @@ -1306,10 +1306,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 5.86, + "value": 6.02, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.memgraph": { @@ -1320,7 +1320,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 4.16, @@ -1334,7 +1334,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 8.31, @@ -1348,10 +1348,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 11.32, + "value": 10.11, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars": { @@ -1362,10 +1362,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 4.79, + "value": 4.58, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars_gpu": { @@ -1376,10 +1376,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 4.71, + "value": 4.56, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars_gpu_vs_pandas": { @@ -1393,10 +1393,10 @@ "graphbench.20k.q3.polars_gpu", "graphbench.20k.q3.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.4, + "value": 2.2, "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q3.polars_vs_pandas": { @@ -1410,10 +1410,10 @@ "graphbench.20k.q3.polars", "graphbench.20k.q3.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.4, + "value": 2.2, "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q4.kuzu": { @@ -1424,10 +1424,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 3.4, + "value": 3.2, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.memgraph": { @@ -1438,7 +1438,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 6.38, @@ -1452,7 +1452,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 11.54, @@ -1466,10 +1466,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 9.88, + "value": 8.75, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars": { @@ -1480,10 +1480,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 3.58, + "value": 3.38, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars_gpu": { @@ -1494,10 +1494,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 16.66, + "value": 16.41, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars_gpu_vs_pandas": { @@ -1511,10 +1511,10 @@ "graphbench.20k.q4.polars_gpu", "graphbench.20k.q4.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 0.6, + "value": 0.5, "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q4.polars_vs_pandas": { @@ -1528,10 +1528,10 @@ "graphbench.20k.q4.polars", "graphbench.20k.q4.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.8, + "value": 2.6, "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q5.kuzu": { @@ -1542,10 +1542,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 5.23, + "value": 5.82, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.memgraph": { @@ -1556,7 +1556,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 4.63, @@ -1570,7 +1570,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 18.33, @@ -1584,10 +1584,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 79.54, + "value": 76.67, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars": { @@ -1598,10 +1598,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 4.1, + "value": 3.84, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars_gpu": { @@ -1612,10 +1612,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 7.21, + "value": 7.34, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars_gpu_vs_pandas": { @@ -1629,10 +1629,10 @@ "graphbench.20k.q5.polars_gpu", "graphbench.20k.q5.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 11.0, + "value": 10.4, "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q5.polars_vs_pandas": { @@ -1646,10 +1646,10 @@ "graphbench.20k.q5.polars", "graphbench.20k.q5.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 19.4, + "value": 20.0, "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q6.kuzu": { @@ -1660,10 +1660,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 8.9, + "value": 8.7, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.memgraph": { @@ -1674,7 +1674,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 4.85, @@ -1688,7 +1688,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 20.99, @@ -1702,10 +1702,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 81.02, + "value": 77.99, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars": { @@ -1716,10 +1716,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 4.99, + "value": 4.81, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars_gpu": { @@ -1730,10 +1730,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 8.03, + "value": 7.18, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars_gpu_vs_pandas": { @@ -1747,10 +1747,10 @@ "graphbench.20k.q6.polars_gpu", "graphbench.20k.q6.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 10.1, + "value": 10.9, "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q6.polars_vs_pandas": { @@ -1764,10 +1764,10 @@ "graphbench.20k.q6.polars", "graphbench.20k.q6.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 16.3, + "value": 16.2, "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q7.kuzu": { @@ -1778,10 +1778,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 5.22, + "value": 5.31, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.memgraph": { @@ -1792,7 +1792,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 5.39, @@ -1806,7 +1806,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 15.14, @@ -1820,10 +1820,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 20.03, + "value": 18.16, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars": { @@ -1834,10 +1834,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 3.26, + "value": 3.09, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars_gpu": { @@ -1848,10 +1848,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 8.35, + "value": 8.57, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars_gpu_vs_pandas": { @@ -1865,10 +1865,10 @@ "graphbench.20k.q7.polars_gpu", "graphbench.20k.q7.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.4, + "value": 2.1, "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q7.polars_vs_pandas": { @@ -1882,10 +1882,10 @@ "graphbench.20k.q7.polars", "graphbench.20k.q7.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 6.2, + "value": 5.9, "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q8.kuzu": { @@ -1896,10 +1896,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 2.77, + "value": 2.79, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.memgraph": { @@ -1910,7 +1910,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 198.8, @@ -1924,7 +1924,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 198.71, @@ -1938,10 +1938,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 12.5, + "value": 7.41, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars": { @@ -1952,10 +1952,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 2.57, + "value": 2.66, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars_gpu": { @@ -1966,10 +1966,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 2.6, + "value": 2.17, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars_gpu_vs_pandas": { @@ -1983,10 +1983,10 @@ "graphbench.20k.q8.polars_gpu", "graphbench.20k.q8.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 4.8, + "value": 3.4, "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q8.polars_vs_pandas": { @@ -2000,10 +2000,10 @@ "graphbench.20k.q8.polars", "graphbench.20k.q8.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 4.9, + "value": 2.8, "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q9.kuzu": { @@ -2014,10 +2014,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 10.98, + "value": 10.69, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.memgraph": { @@ -2028,7 +2028,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 124.91, @@ -2042,7 +2042,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", "value": 422.29, @@ -2056,10 +2056,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 22.32, + "value": 16.2, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars": { @@ -2070,10 +2070,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 7.79, + "value": 7.68, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars_gpu": { @@ -2084,10 +2084,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "ms", - "value": 6.89, + "value": 6.95, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars_gpu_vs_pandas": { @@ -2101,10 +2101,10 @@ "graphbench.20k.q9.polars_gpu", "graphbench.20k.q9.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 3.2, + "value": 2.3, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q9.polars_vs_pandas": { @@ -2118,10 +2118,10 @@ "graphbench.20k.q9.polars", "graphbench.20k.q9.pandas" ], - "run": "graphbench-q1q9-20k-20260904", + "run": "graphbench-q1q9-20k-master-20260905", "status": "ok", "unit": "x", - "value": 2.9, + "value": 2.1, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" }, "graphframes.friendster.filter.gfql_polars": { @@ -3447,15 +3447,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 1.686, + "value": 1.659, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.gfql_polars": { @@ -3478,15 +3477,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 3.059, + "value": 3.469, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.kuzu": { @@ -3542,15 +3540,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 2.345, + "value": 2.867, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.gfql_polars": { @@ -3573,15 +3570,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 6.812, + "value": 6.396, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.kuzu": { @@ -3637,15 +3633,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 14.538, + "value": 13.99, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.gfql_polars": { @@ -3668,15 +3663,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 12.977, + "value": 13.138, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.kuzu": { @@ -3700,15 +3694,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 73.506, + "value": 70.572, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.gfql_polars": { @@ -3731,15 +3724,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 42.903, + "value": 44.467, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.kuzu": { @@ -3763,15 +3755,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 50.924, + "value": 47.675, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.gfql_polars": { @@ -3794,15 +3785,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 35.658, + "value": 35.613, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.kuzu": { @@ -3858,15 +3848,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 3.908, + "value": 3.921, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.gfql_polars": { @@ -3889,15 +3878,14 @@ "comparison_allowed": true, "decimals": 3, "disclosures": [ - "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run." + "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 10.836, + "value": 11.997, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.kuzu": { @@ -3954,15 +3942,14 @@ "decimals": 3, "disclosures": [ "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run.", "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "result_mismatch", "unit": "ms", - "value": 61.173, + "value": 59.461, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf01.tag_cooccurrence.gfql_polars_idx": { @@ -3971,15 +3958,14 @@ "decimals": 3, "disclosures": [ "GFQL arms with resident indexes (gfql_index_all + node property indexes, verified per query) under the same timing contract as the four-engine release board (2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs, median of run medians); the index engages only on the hop seams (message replies, recent replies); the point lookups still pay the chain pipeline's per-call floor (pygraphistry #2027).", - "SF0.1 GFQL arms measured at the head of pull request #2038 (commit cdbcc640a: native chain seeds resolve through the resident indexes on pandas, cuDF and polars, on #2037, #2035, #2031 and #2030), not yet on master when measured; the release-candidate run on master replaces these cells. SF1 cells are the 0.59.0 run.", "tag-cooccurrence: not comparable with the four-engine board (parity, eligibility, or cardinality differs from the release GFQL arm)." ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-2038-20260905", + "run": "snb-master-5a6586f22-20260905", "status": "result_mismatch", "unit": "ms", - "value": 26.952, + "value": 31.665, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf1.is3.single_query.polars": { @@ -4021,10 +4007,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 67.631, + "value": 1.682, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, "snb.sf1.message_content.gfql_polars": { @@ -4051,10 +4037,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 4.251, + "value": 2.991, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, "snb.sf1.message_content.kuzu": { @@ -4114,10 +4100,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 344.293, + "value": 2.381, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, "snb.sf1.message_creator.gfql_polars": { @@ -4144,10 +4130,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 43.607, + "value": 5.897, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, "snb.sf1.message_creator.kuzu": { @@ -4207,10 +4193,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 571.088, + "value": 533.559, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, "snb.sf1.new_topics.gfql_polars": { @@ -4237,10 +4223,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 163.878, + "value": 162.322, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, "snb.sf1.new_topics.kuzu": { @@ -4268,10 +4254,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 359.515, + "value": 4.042, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, "snb.sf1.seed_lookup.gfql_polars": { @@ -4298,10 +4284,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-aligned-indexed-20260904", + "run": "snb-master-5a6586f22-20260905", "status": "ok", "unit": "ms", - "value": 45.793, + "value": 10.719, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, "snb.sf1.seed_lookup.kuzu": { @@ -4354,8 +4340,8 @@ } }, "contract_version": 3, - "generated_at": "2026-09-05T03:02:45Z", - "generated_by": "graphistry/pyg-bench 2ffd92d9 scripts/export_docs_numbers.py", + "generated_at": "2026-09-06T01:42:41Z", + "generated_by": "graphistry/pyg-bench c6c781e0 scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { @@ -4413,33 +4399,33 @@ "row_validation": "All 12 selected-node sets had Jaccard 1.0 against the reference (gate 0.95); every slot passed typed result, exact run-contract, and load/self-spike validation; the committed aggregate exactly recomputes from all slots.", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13); GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-100k-20260904": { - "artifact": "results/graphbench-059-gpu-100k-20260904 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-100k-master-20260905": { + "artifact": "results/graphbench-master-5a6586f22-100k-20260905 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 100,000 persons / 2,417,738 FOLLOWS / 2,775,195 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-04", + "measured_at": "2026-09-06", "perf_lock_held": true, - "pyg_bench_commit": "d028118", - "pygraphistry_commit": "3fb216dd", + "pyg_bench_commit": "5cf81ae", + "pygraphistry_commit": "5a6586f22", "quiet_host": true, - "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-20k-20260904": { - "artifact": "results/graphbench-059-gpu-20k-20260904 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-20k-master-20260905": { + "artifact": "results/graphbench-master-5a6586f22-20k-20260905 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 20,000 persons / 199,939 FOLLOWS / 260,107 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-04", + "measured_at": "2026-09-06", "perf_lock_held": true, - "pyg_bench_commit": "d028118", - "pygraphistry_commit": "3fb216dd", + "pyg_bench_commit": "5cf81ae", + "pygraphistry_commit": "5a6586f22", "quiet_host": true, - "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board.", + "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, @@ -4473,36 +4459,6 @@ "row_validation": "every task records the materialized result size (node count of the filter / k-hop ball, scored vertex count for PageRank); a cell is comparable only when every system on that dataset and task reports the same size, else it is published as a direct, diagnostic-only time", "runtime": "GFQL in graphistry/test-rapids-official:26.02-gfql-polars (RAPIDS 26.02, Python 3.13, polars 1.35.2, cuDF/cuGraph 26.2) under dgx-guard safe_run (RMM 80 GB, 100 GB for the Friendster rungs; host floor 20 GB); the two CPU PageRank rungs (fix2024/lj-polars-pagerank, orkut-polars-pagerank) in graphistry/test-rapids-official:26.02-gfql-polars-igraph, the same image plus python-igraph 1.0.0 (image id and versions in each rung's IMAGE_RECEIPT.txt, recorded after the run); GraphFrames on host PySpark 3.5.3 + graphframes assembly jar, OpenJDK 1.8.0_492" }, - "snb-aligned-indexed-20260904": { - "artifact": "results/snb-aligned-indexed-20260904 (SF1 cells; its SF0.1 cells are superseded by run snb-aligned-indexed-2038-20260905)", - "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", - "competitor_version": "n/a", - "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board's parameters)", - "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-04", - "perf_lock_held": true, - "pyg_bench_commit": "1ee6135", - "pygraphistry_commit": "3fb216dd", - "quiet_host": true, - "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", - "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", - "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" - }, - "snb-aligned-indexed-2038-20260905": { - "artifact": "results/snb-aligned-indexed-2038-20260905 (per-engine r1..r3 receipts + aggregate.json built with scripts/aggregate_snb_arm_runs.py from the release runner's run directories)", - "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", - "competitor_version": "n/a", - "dataset": "LDBC SNB Interactive SF0.1, derived matched query shapes (the release board's parameters)", - "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-05", - "perf_lock_held": true, - "pyg_bench_commit": "dad44f5", - "pygraphistry_commit": "cdbcc640a", - "quiet_host": true, - "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", - "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", - "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" - }, "snb-aligned-release-20260902": { "artifact": "results/snb-aligned-release-20260902", "competitor": "Kuzu, Neo4j, Memgraph", @@ -4532,6 +4488,21 @@ "reps": "3 repetitions of configs/suites/ab-is1-is7-sf1-polars.yaml, each already a median of 5 measured repeats after 1 warmup; median across repetitions", "row_validation": "920/920 rows, value-identical to the independently derived expected fixture in all 3 repetitions", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" + }, + "snb-master-5a6586f22-20260905": { + "artifact": "results/snb-master-5a6586f22 (per-engine r1..r3 receipts + aggregate.json built with scripts/aggregate_snb_arm_runs.py from the release runner's run directories at pygraphistry master)", + "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", + "competitor_version": "n/a", + "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board's parameters)", + "host": "dgx-spark (NVIDIA GB10), 20 CPU", + "measured_at": "2026-09-05", + "perf_lock_held": true, + "pyg_bench_commit": "5cf81ae", + "pygraphistry_commit": "5a6586f22", + "quiet_host": true, + "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", + "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", + "runtime": "GFQL polars / pandas on CPU in a uv Python 3.12 container, gfql_index: use with node property indexes (auto) and per-query engagement verification; same host, lock and contract as the 2026-09-02 four-engine board" } }, "schema_version": 1 diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 9de701afa7..df1dde6081 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -36,8 +36,8 @@ wins on both (:bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` and :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` of its speed). The GFQL filter and hop rows were measured at the head of the fix for `#2023 `_ -(`#2024 `_, not yet on master when -measured); the released code's LiveJournal 2-hop was +(`#2024 `_, measured at that pull +request's head; it has since landed on master and the ladder was not re-run); the released code's LiveJournal 2-hop was :bench-diag:`graphframes_059.lj.hop2.gfql_polars`, the before-state the disclosures keep. .. image:: _static/graphframes/livejournal_tasks.svg diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index e8ce0e6cb6..35f1af03b6 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -97,10 +97,9 @@ igraph (`#2032 `_); a po native chain whose edge alias collides with the edge column its own filter uses raises where pandas serves it (`#2039 `_); ``rows(table=nodes, source=alias)`` multiplies rows for duplicate node ids -(`#2034 `_). The SF0.1 GFQL -cells were measured at the head of the pull request that landed the index-resolved seeds, -before it reached master; the SF1 cells are the release measurement, and the Measurement -block below carries both. +(`#2034 `_). The SF0.1 and SF1 GFQL +cells are the release measurement on landed master; the Measurement block below carries the +provenance. SF0.1 ~~~~~ @@ -160,7 +159,7 @@ Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.js which pyg-bench publishes. The documentation build and ``docs/test_bench_numbers.py`` reject missing, stale, or unpublished values. -.. bench-provenance:: graphbench-q1q9-20k-20260904 graphbench-q1q9-100k-20260904 snb-aligned-release-20260902 snb-aligned-indexed-20260904 snb-aligned-indexed-2038-20260905 +.. bench-provenance:: graphbench-q1q9-20k-master-20260905 graphbench-q1q9-100k-master-20260905 snb-aligned-release-20260902 snb-master-5a6586f22-20260905 :disclosures: Next steps From 3dbf8fcae0bd2dba07a8119c630c792fee9a3c4f Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 7 Sep 2026 11:48:28 -0700 Subject: [PATCH 27/29] docs(gfql): engines page rewritten for readers, perf hub by reader intent, docs-review editorial policy engines: one keyword section replaces the duplicated opening and switching sections; footnote citations folded into the decision table and three plain rules; 'when not to use Polars' merged into the table; parity section cut to three bullets; internal process prose removed; competitor table states benefits; plain sentences in the memory note. perf hub: case study first, then the boards, then a separate track for getting the speed on your data, with a reference list. agents/skills/docs-review: the editorial policy (plain language, jargon table, AI-writing tells, review procedure). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HdYcPgiafGGutW9KfG3gg1 --- agents/skills/docs-review/SKILL.md | 66 ++++ docs/source/gfql/engines.rst | 559 ++++++++++------------------- docs/source/gfql/perf/index.rst | 56 ++- 3 files changed, 290 insertions(+), 391 deletions(-) create mode 100644 agents/skills/docs-review/SKILL.md diff --git a/agents/skills/docs-review/SKILL.md b/agents/skills/docs-review/SKILL.md new file mode 100644 index 0000000000..e1a42e9ebe --- /dev/null +++ b/agents/skills/docs-review/SKILL.md @@ -0,0 +1,66 @@ +--- +name: docs-review +description: Editorial policy for user-facing documentation (docs/source/**). Use when writing or reviewing prose in docs pages, docstrings that render in docs, or release notes. Plain, direct, forward-readable text; no internal process talk; no AI-writing tells. +--- + +# Docs editorial policy + +Audience: a pandas, Polars, or Cypher user who has never seen this codebase. They read once, +forwards, and stop at the first sentence that does not pay off. + +## Rules (ASD-STE100 in spirit) + +- One idea per sentence, about 20 words, active voice, present tense. +- Verbs over nouns: "the index reads only those neighborhoods", not "the index enables + neighborhood-scoped reads". +- Say the benefit to the reader, then the mechanism, then the evidence link. Never the + mechanism alone. +- Define or replace jargon on first use. Prefer the plain phrase (right column): + + | avoid | write | + |---|---| + | shape (of a query) | kind of query, query pattern, workload | + | seeded / seed set / seeded lookup | a query that starts from a few known nodes | + | lane, route, fast path, hot path | (omit; say what runs faster and when) | + | parity, oracle | the same result on every engine | + | decline, typed decline | raises an error before the query runs | + | materialize (intermediate) | build an intermediate result | + | frontier | the nodes reached at this hop | + | point lookup | a query for one node by id | + | receipt, artifact, committed artifact | (omit; link the provenance section once) | + | attenuation, engagement | (omit) | + | release gate, CI, sweep, lever, step N | (omit; internal process) | + +- No parentheticals inside sentences. Split them into sentences or delete them. +- No unnecessary contrast flourishes: "not X but Y", "no GPU, same results", "X — and Y". + State Y. +- No mannerist titles ("The one-line speedup"). Title = what the reader gets to do + ("Switch engines with one keyword"). +- No footnote-style citations in body text ("[F1] Polars leads"). Put one fact inline with + its link, or move the block to a provenance section. +- No competitor ammunition. Say where GFQL is good and link the full board with the + losses shown. Do not editorialize a competitor's strengths. +- No internal process in user docs: release gates, CI lanes, what we refuse to publish, + how numbers were audited. One provenance line with a link is enough. +- Every number comes from a vendored benchmark cell (`:bench-*:` roles). Never type a + measured number as a literal. +- Keep examples and tables; cut prose. A rewrite that shortens prose by a third with no + loss of meaning is the normal outcome of a review. + +## AI-writing tells to remove + +- Triads for rhythm ("fast, safe, and simple"), stacked em-dash asides, "not only … but". +- Sentences that restate the previous one with more adjectives. +- "Deliberately", "carefully", "seamlessly", "robust", "powerful", "leverage". +- Rhetorical questions and "so what does this mean?" transitions. +- Claims about the writing itself ("this page is honest about losses"). + +## Review procedure + +1. Read the page forwards once as the target reader. Mark every stop. +2. For each section ask: what is the one message that is powerful to deliver? If none, + delete the section. If one, rewrite the section to deliver it in the fewest sentences. +3. Apply the jargon table, then the tells list. +4. Check every link and every `:bench-*:` role still resolves; run + `python -m pytest docs/test_bench_numbers.py`. +5. Post the ReadTheDocs preview link per changed page with the sections to check. diff --git a/docs/source/gfql/engines.rst b/docs/source/gfql/engines.rst index 074723fe36..582a139c3e 100644 --- a/docs/source/gfql/engines.rst +++ b/docs/source/gfql/engines.rst @@ -3,23 +3,16 @@ Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU ======================================================== -GFQL runs the **same query** on four interchangeable execution engines. You pick -the engine with one keyword — ``engine=``, accepted uniformly by ``g.gfql()`` and -``g.hop()`` — and GFQL returns **identical results** on every one (differential parity -is a release gate). Unsupported engine/query combinations are declined during -validation, compilation, or planning before query execution whenever they can be -known statically, so the safety contract is same answer or pre-execution error, -not silent fallback. Pick the engine that fits your hardware and workload; nothing -else changes. - -This page assumes you already have a graph ``g`` and a ``query``; if not, start with -:doc:`about`. +GFQL runs the same query on four execution engines. You choose the engine with one +keyword, ``engine=``, on ``g.gfql()`` and ``g.hop()``. Every engine returns the same +result. When an engine cannot run a query, GFQL raises an error before the query runs +instead of switching engines behind your back. -The one-line speedup --------------------- +This page assumes you have a graph ``g`` and a ``query``. If not, start with +:doc:`about`. -On real graphs, switching the default ``pandas`` engine to the columnar **Polars** -engine is a one-keyword change — no GPU, same results: +Switch engines with one keyword +------------------------------- .. doc-test: skip @@ -29,24 +22,31 @@ engine is a one-keyword change — no GPU, same results: g = graphistry.edges(df, 'src', 'dst') # df: your edges dataframe (pandas / Polars / cuDF) query = "MATCH (a)-[e]->(b) RETURN b" # any GFQL / Cypher query - g.gfql(query) # engine='pandas' (default) - g.gfql(query, engine='polars') # often much faster on query-heavy workloads, identical results + g.gfql(query) # engine='pandas' (default) + g.gfql(query, engine='polars') # CPU, columnar + g.gfql(query, engine='cudf') # NVIDIA GPU (RAPIDS) + g.gfql(query, engine='polars-gpu') # the Polars plan on the GPU -On the ``prrao87/graph-benchmark`` q1–q9 Cypher suite, Polars beats pandas on all nine -queries at both graph sizes measured, and by more on the larger one. The per-query -numbers are on the :doc:`performance` page. +Polars is the usual first move. On the ``prrao87/graph-benchmark`` q1–q9 Cypher suite it +beats pandas on all nine queries at both graph sizes measured, without a GPU. The +per-query numbers are on the :doc:`performance` page. -Your existing pandas, Polars, or cuDF graph works as-is: the input frames are accepted and -coerced once; the only change is the keyword. The catch: a few exotic Cypher features still -require ``engine='pandas'`` (they decline during validation, compilation, or planning rather -than silently bridge), and the GPU engines only pay off on larger work. On CPU, -Polars wins the common graph-query shapes (traversal, -``WHERE``/``ORDER``, aggregation) — see *When not to use Polars* below. +Your existing pandas, Polars, or cuDF graph works as-is. GFQL converts the input frames +once, at the start of the call. Results come back in the engine's frame type: Polars +frames for ``'polars'`` and ``'polars-gpu'``, ``cudf.DataFrame`` for ``'cudf'``. Convert +once when downstream code needs pandas: -**Already a Polars user?** With the default ``engine='auto'``, a graph whose bound frames -are all ``polars.DataFrame`` runs on the Polars engine and returns Polars frames. If the -query uses a shape the Polars engine declines, ``auto`` falls back to pandas for that call. -Pass ``engine='polars'`` when a decline should raise instead: +.. doc-test: skip + +.. code-block:: python + + out = g.gfql(query, engine='polars') # or 'cudf' / 'polars-gpu' + nodes_pd = out._nodes.to_pandas() # pandas for matplotlib, scikit-learn, .iloc, ... + +**Already a Polars user?** The default ``engine='auto'`` follows your frames: a graph built +from Polars frames runs on the Polars engine and returns Polars frames, and a cuDF graph +runs on ``cudf``. If a query uses a feature the Polars engine does not support, ``auto`` +runs that call on pandas. Pass ``engine='polars'`` when you want an error instead: .. doc-test: skip @@ -54,12 +54,8 @@ Pass ``engine='polars'`` when a decline should raise instead: import polars as pl, graphistry g = graphistry.edges(edges_pl, 'src', 'dst').nodes(nodes_pl, 'id') # polars frames - out = g.gfql(query) # auto -> native Polars (out._nodes is polars) - out = g.gfql(query, engine='polars') # same, but a declined shape raises - -**Result frames match the engine.** With ``engine='polars'`` or ``'polars-gpu'`` the output -frames are Polars, and ``cudf.DataFrame`` for ``engine='cudf'``. Pandas-only downstream code -(``.iloc``, ``groupby().apply()``) gets a pandas frame with ``result._nodes.to_pandas()``. + out = g.gfql(query) # auto -> Polars engine (out._nodes is polars) + out = g.gfql(query, engine='polars') # same, but an unsupported feature raises The four engines ---------------- @@ -77,117 +73,36 @@ The four engines - CPU - ``pandas`` - default - - Universal default; best on small/interactive graphs. + - Works everywhere; best for small, interactive graphs. * - ``polars`` - CPU - ``polars`` - explicit - - Columnar + fused lazy plan; the CPU speed win, **no GPU needed**. + - Columnar, one fused plan; the CPU speed win, no GPU needed. * - ``cudf`` - NVIDIA GPU - ``cudf`` - explicit - - RAPIDS GPU, eager op-by-op; great for one very large materialization. + - RAPIDS GPU, one operation at a time; best for one very large result. * - ``polars-gpu`` - NVIDIA GPU - ``polars`` - explicit - - The Polars fused plan executed on GPU (cudf_polars); fastest on heavy multi-hop. - -``engine='auto'`` follows the input frames: Polars frames run on ``polars``, cuDF frames on -``cudf``, everything else on ``pandas``. Two AUTO fast paths go further — all-Polars frames -are tried on ``polars``, and all-cuDF frames are tried on ``polars-gpu`` when a GPU collect -probes usable — each falling back to ``pandas`` / ``cudf`` respectively if the query uses a -shape that engine declines. Passing the engine explicitly turns those declines into errors -instead of a fallback (see *What auto does* below). - -How the engines compare ------------------------ - -Each engine has a shape it is built for: - -- **Polars-CPU is the everyday win.** It beats pandas on all nine queries of the q1–q9 - Cypher suite at both sizes measured (:doc:`performance`), with **no GPU**, because it - builds **one fused lazy plan and collects once** instead of materializing an - intermediate per operation. -- **Polars-GPU runs that same fused plan on the GPU.** It pays off once a step carries - enough work to amortize a kernel launch: large frontiers, dense joins, full-graph - aggregation. -- **cuDF executes eagerly, op by op.** That suits one very large materialization where a - single join dominates the query and the in-memory Polars-GPU executor comes under - memory pressure. -- **pandas carries no plan overhead**, so it stays the right default for trivially small - operations and the widest-compatibility path. -- **Seeded / selective lookups are an indexing problem**, not an engine race: the opt-in - resident index turns the ``O(E)`` scan into an ``O(degree)`` gather on every engine, so - the cost tracks the seeds rather than the graph — see [F5] below and - :doc:`index_adjacency`. + - The Polars fused plan run on the GPU (cudf_polars); fastest on heavy multi-hop work. -.. _gfql-vs-external-tools: - -Coming from another graph tool ------------------------------- - -GFQL is **dataframe-native**: ``pip install``, then query the pandas, Polars, or cuDF frame -you already have, in your own process. There is no server to stand up, no ETL to load, no -projection step, no cluster to size. The query, the analytic, and the scoring stay in one -pipeline over one set of frames. +Polars builds one plan for the whole query and runs it once. pandas and cuDF run the +query one operation at a time and materialize each intermediate result. That difference +is why Polars leads on CPU, why ``polars-gpu`` leads on heavy multi-hop work, and why CPU +Polars often beats cuDF on bulk work. -The table names the concrete change for each system and where the measured comparison -lives. Every figure on those pages renders from a committed pyg-bench artifact. - -.. list-table:: - :header-rows: 1 - :widths: 16 30 54 - - * - Coming from - - What changes - - What you gain, and where it is measured - * - **Neo4j + GDS** - - Same ``MATCH ... RETURN`` Cypher; no server, no GDS projection, no write-back. - - One in-process call runs filter, PageRank, and filter over resident frames, on CPU - or GPU. The measured pipeline times against Neo4j + GDS on the 30M-edge GPlus graph - are in :doc:`benchmark_filter_pagerank`. - * - **Memgraph** - - Same Cypher; no server round trip. - - Point lookups are Memgraph's strength: on the SNB-derived point queries the graph - databases, Memgraph first, beat GFQL. GFQL's wins are bulk shapes: traversals from - seed sets and global aggregates. See :doc:`performance`. - * - **Kuzu** - - Same Cypher; query the frame already in memory, nothing to load or index first. - - The q1–q9 board on :doc:`performance` is the measured comparison, per query, with - the losses shown. - * - **LadybugDB** - - Same dataframe-native path, in process. - - Polars streaming (``GFQL_POLARS_CPU_STREAMING=1``) and the cudf-polars streaming - executor (``GFQL_POLARS_GPU_EXECUTOR=streaming``) spill intermediates and results - beyond RAM. Scan-shaped queries are measured on :doc:`performance`. - * - **networkx** - - A declarative query language over the graph, on frames instead of Python objects. - - Columnar CPU execution and a one-keyword move to GPU. - * - **igraph** - - Nothing to give up: igraph is GFQL's CPU PageRank backend. - - The query layer, the Polars engines, and the GPU path on top of igraph analytics. - * - **Spark GraphFrames** - - Cypher instead of a DataFrame API; single node, no cluster. - - Interactive latency for filters and traversals on CPU, and GPU PageRank. The - head-to-head, with committed results, is :doc:`benchmark_graphframes`. - -Route by shape: **selective** seeded lookups favor the GFQL resident index, **scan and -aggregate** volume favors Polars, and **bulk** frontier expansion and full pipelines favor -Polars or a GPU engine. - -What is **not** benchmarked: motif, triangle, and other cyclic multi-way-join patterns. -They run in GFQL, and we publish no performance claim about them. - -Decision matrix ---------------- +Which engine for which work +--------------------------- .. list-table:: :header-rows: 1 :widths: 30 16 18 22 14 - * - Workload shape + * - Workload - Size (edges) - Hardware - Recommended engine @@ -196,156 +111,135 @@ Decision matrix - past small/interactive - CPU - ``polars`` - - takes over past small graphs; gap grows with size [F1] - * - Bulk 1-hop frontier expansion + - the gap over pandas grows with graph size + * - Bulk 1-hop expansion - past small/interactive - CPU - ``polars`` - - takes over past small graphs; gap grows with size [F1] + - the gap over pandas grows with graph size * - Heavy multi-hop (2-hop+) - large - GPU - ``polars-gpu`` - - fastest until extreme materialization [F3]; GPU-or-error [F4] + - fastest until one step produces an extreme result size * - Full-graph aggregation - very large - GPU - ``polars-gpu`` / ``cudf`` - - GPU work-bound [F2] - * - One very large single materialization + - the GPU wins once there is enough work per step + * - One very large single result - huge output row count - GPU - ``cudf`` - - Polars-GPU can hit memory pressure here [F3] - * - Trivially small op (bare equality filter) + - ``polars-gpu`` can run short of GPU memory here + * - Trivially small operation (one equality filter) - any - CPU - ``pandas`` - - boolean mask beats Polars plan overhead; immaterial [F1] - * - Selective / seeded traversal + - pandas avoids the plan overhead; the difference is microseconds + * - Query starting from a few known nodes + - any + - CPU + - ``pandas`` / ``polars`` + adjacency index + - cost follows the neighborhood, not the graph; see below + * - Cypher features the Polars engine does not support yet - any - CPU - - ``pandas``/``polars`` + **CSR index** - - O(degree), not an engine choice [F5] - -**[F1] Polars leads on CPU, and by more as the graph grows.** On the q1–q9 Cypher suite -it beats pandas on all nine queries at both sizes measured, and the pandas-to-Polars gap -is wider on the larger graph for every query (:doc:`performance`). -Pandas only edges out on a trivially small operation (a bare equality mask), -where the absolute difference is immaterial. The real small-size floor is **GPU-only** — -cuDF / Polars-GPU need enough work to amortize kernel launch ([F2]). - -**[F2] GPU is work-bound, not size-bound.** A GPU wins when there is enough work to amortize -its millisecond-scale kernel-launch floor: big frontiers, dense joins, full-graph -aggregation. Tiny or seeded work finishes faster on CPU. - -**[F3] Polars-GPU memory pressure.** On an extreme single materialization (a huge output -row count from one join) raw ``cudf`` leads and ``polars-gpu`` slips as its in-memory -GPU executor comes under memory pressure. Prefer ``cudf`` for that regime. - -**[F4] Polars-GPU is GPU-or-error.** It never silently falls back to CPU and reports the -result as a GPU run (see *Parity and fallback rules* below). - -**[F5] Selective traversal is an indexing problem, not an engine choice.** A seeded ``hop`` -from a few nodes is fastest with the opt-in **CSR adjacency index** (``g.gfql_index_all()`` / -``g.create_index(...)``, ``index_policy=``), which turns the O(E) scan into an O(degree) -gather — a complexity-class change, so the cost tracks the seeds' neighborhood rather than -the graph (index routing for the native seeded ``g.hop()`` currently engages on pandas, not -yet the Polars hop path). It works on all four engines, but seeded work is so small that -**CPU wins** — the gather is tiny work on pandas/Polars, below the GPU kernel-launch floor -on cuDF — the clean inverse of bulk, where the GPU pulls ahead. So pick the index for selective -traversal and a CPU engine to drive it. See :doc:`index_adjacency` for the full guide. - -Switching engines ------------------ - -The engine is a single keyword on ``g.gfql()`` (and ``g.hop()``). The graph and -the query never change — only ``engine=`` does, and the answer stays identical -(or the compiler/planner declines the unsupported engine before execution rather -than silently changing it). + - ``pandas`` + - the Polars engine raises before running; ``auto`` runs the call on pandas -.. code-block:: python +Three rules cover most decisions: - import graphistry - g = graphistry.edges(df, 'src', 'dst') # your existing graph (any frame type) - query = "MATCH (a)-[e]->(b) RETURN b" # any GFQL / Cypher query +- **A GPU pays off by work, not by graph size.** Each GPU step costs about a millisecond + to launch. Large frontiers, dense joins and full-graph aggregation cover that cost; + small work finishes faster on CPU. +- **Queries that start from a few known nodes are an indexing problem.** Build the + adjacency index once with ``g.gfql_index_all()`` and a traversal from a watchlist, a + session, or a seed set reads only those nodes' neighborhoods instead of scanning every + edge. The work is then small, so drive it from a CPU engine. See :doc:`index_adjacency`. +- **Polars-GPU runs on the GPU or raises.** It never runs a plan on the CPU and reports it + as a GPU result. - g.gfql(query) # engine='pandas' (default) - g.gfql(query, engine='polars') # CPU columnar, no GPU, identical results - g.gfql(query, engine='cudf') # NVIDIA GPU (RAPIDS) - g.gfql(query, engine='polars-gpu') # same fused plan on GPU +.. _gfql-vs-external-tools: -Getting results back as pandas -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Coming from another graph tool +------------------------------ -The result's ``._nodes`` / ``._edges`` come back in the engine's frame type: a -``polars.DataFrame`` for ``'polars'`` / ``'polars-gpu'``, a ``cudf.DataFrame`` -for ``'cudf'``. When downstream code is pandas-only (matplotlib, scikit-learn, -``.iloc`` / ``groupby().apply()``), convert once with ``.to_pandas()``: +GFQL runs inside your Python process on the pandas, Polars, or cuDF frame you already +have. There is no database server to run, no load step, no projection, and no cluster to +size. The query, the analytic and the scoring run in one pipeline over one set of frames. -.. code-block:: python +.. list-table:: + :header-rows: 1 + :widths: 16 30 54 - out = g.gfql(query, engine='polars') # or 'cudf' / 'polars-gpu' - nodes_pd = out._nodes.to_pandas() # -> pandas for matplotlib / sklearn / ... - nodes_pd.plot.scatter(x='x', y='y') # pandas-only downstream code, unchanged - -Mixing engines -~~~~~~~~~~~~~~~ - -The build frame type and the run engine are independent — GFQL coerces the input -frames to the engine you ask for. A pandas graph runs on ``engine='polars'``, a -Polars graph runs on ``engine='pandas'``, and so on. The only cost is a -**one-time convert** of the input frames at the start of the call; the query then -runs fully on the chosen engine. Note that ``engine='auto'`` (the default) follows -the input frames — Polars frames run natively on ``polars``, cuDF frames on -``cudf`` (or ``polars-gpu`` when that GPU path probes usable), everything else on -``pandas`` — falling back to ``pandas`` / ``cudf`` only for query shapes the native -engine declines. - -.. tip:: - For selective, seeded traversal, build the CSR adjacency index once with - ``g.gfql_index_all()`` (or ``index_policy=``) — it works on all four engines - and turns the O(E) scan into an O(degree) gather. An AUTO build on Polars frames now - keeps them native, so ``g.gfql_index_all()`` and ``g.gfql_index_all(engine='polars')`` - build the same index. See :doc:`index_adjacency`. + * - Coming from + - What changes + - What you gain, and where it is measured + * - **Neo4j + GDS** + - The same ``MATCH ... RETURN`` Cypher. + - GFQL removes the database server, the GDS projection step, and the write-back. One + call runs the filter, PageRank and scoring over frames that are already in memory, + on CPU or GPU, so a pipeline is one function instead of three systems. Measured + pipeline times against Neo4j + GDS on the 30M-edge GPlus graph: + :doc:`benchmark_filter_pagerank`. + * - **Memgraph** + - The same Cypher. + - GFQL removes the server round trip and keeps results as dataframes. GFQL is at its + best on traversals from seed sets and on global aggregates; the SNB-derived + comparison on :doc:`performance` shows each query, including the ones the graph + databases win. + * - **Kuzu** + - The same Cypher. + - Query the frame already in memory, with nothing to load or index first. The q1–q9 + board on :doc:`performance` shows every query, wins and losses. + * - **LadybugDB** + - The same dataframe-native approach, in process. + - Streaming modes for results larger than RAM or GPU memory + (:ref:`gfql-larger-than-memory`). Scan-style queries are measured on + :doc:`performance`. + * - **networkx** + - A query language over frames instead of Python object graphs. + - Columnar CPU execution and a one-keyword move to the GPU. + * - **igraph** + - Nothing to give up: igraph is GFQL's CPU PageRank backend. + - A query layer, the Polars engines and the GPU path on top of igraph analytics. + * - **Spark GraphFrames** + - Cypher instead of a DataFrame API; one machine, no cluster. + - Interactive latency for filters and traversals on CPU, and GPU PageRank. The + head-to-head with results: :doc:`benchmark_graphframes`. + +Motif, triangle and other cyclic multi-way-join patterns run in GFQL but are not +benchmarked; this documentation makes no performance claim about them. .. _gfql-offengine-calls: Analytics under Polars (``umap`` / ``hypergraph`` / ``compute_cugraph`` …) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A GFQL ``call()`` that runs a **whole-graph analytic** — ``umap``, ``hypergraph``, -``compute_cugraph`` / ``compute_igraph``, the ``*_layout`` ops, ``collapse`` — has -**no native Polars implementation** (these wrap pandas / cuDF / GPU libraries and -always will). Under ``engine='polars'`` / ``'polars-gpu'`` GFQL runs them as a -**mode-gated, off-engine modality switch** rather than declining outright: - -- **``call_mode='auto'`` (the default):** the analytic runs off-engine — on - **pandas** for ``polars``, on **cuDF (on device)** for ``polars-gpu`` — and its - result is coerced back to Polars **losslessly** (via Arrow). A one-time - ``RuntimeWarning`` per analytic notes the off-engine run. ``polars-gpu`` is - **GPU-or-error**: it bridges to cuDF and *declines* if the GPU/cuDF stack is - missing (it never silently drops a GPU analytic to host pandas). -- **``call_mode='strict'``:** decline before running the analytic instead of - bridging — for benchmark integrity (no hidden modality switch attributed to the - Polars engine) or a hard memory ceiling. +------------------------------------------------------------------------------- + +A GFQL ``call()`` that runs a whole-graph analytic (``umap``, ``hypergraph``, +``compute_cugraph`` / ``compute_igraph``, the ``*_layout`` ops, ``collapse``) wraps a +pandas, cuDF or GPU library and has no Polars implementation. Under ``engine='polars'`` +or ``'polars-gpu'`` these calls run off-engine: + +- **``call_mode='auto'`` (default):** the analytic runs on pandas for ``polars`` and on + cuDF for ``polars-gpu``. The result comes back as Polars frames without loss, and GFQL + warns once per analytic. ``polars-gpu`` raises if the cuDF stack is missing; it never + moves a GPU analytic to the CPU. +- **``call_mode='strict'``:** raise instead of running the analytic off-engine. Use this + when a run must stay on one engine, or to hold a hard memory ceiling. .. note:: - **Memory on a very large graph.** The bridge materializes a copy of the graph in - the off-engine format — pandas (host) for ``polars``, cuDF (device / unified - memory) for ``polars-gpu``. That transient copy is the *same* allocation you'd - incur running the analytic on ``engine='cudf'`` directly, so GFQL does **not** add - a per-call size cap (a row count is a poor memory proxy, and the real cap belongs - at the RMM / container / deployment layer). For a graph large enough that the copy - is a concern, either set ``call_mode='strict'`` (decline the bridge) or run the - analytic under an RMM device-memory limit / container memory limit, exactly as you - would for any cuDF workload. - -This is **deliberately narrower** than traversal / filter / row ops (``hop``, -``WHERE``, ``RETURN`` …), which stay **parity-or-static-decline** and are never -bridged — a bridge there would hide a missing native impl and misreport pandas -performance as Polars. Set the mode from Python or the environment (live, Python -override > env > default): + **Memory on a very large graph.** The off-engine run makes one copy of the graph in + the analytic's format: pandas in host memory for ``polars``, cuDF in device memory for + ``polars-gpu``. This is the same allocation the analytic makes on ``engine='cudf'``. + GFQL sets no size cap of its own. If that copy is a concern, set + ``call_mode='strict'`` or run under an RMM or container memory limit, as for any cuDF + workload. + +Traversal, filter and row operations (``hop``, ``WHERE``, ``RETURN``) are never run +off-engine: they run on the engine you asked for or raise before running. Set the mode +from Python or the environment; a Python setting overrides the environment: .. doc-test: skip @@ -353,35 +247,31 @@ override > env > default): from graphistry.compute.gfql.lazy import set_call_mode, CALL_MODES # ('auto', 'strict') - set_call_mode('strict') # decline off-engine analytics (pass None to reset to env/default) + set_call_mode('strict') # raise on off-engine analytics (pass None to reset to env/default) # or: export GFQL_POLARS_CALL_MODE=strict cuDF vs Polars-GPU ------------------ -Both run on an NVIDIA GPU, so which do you use? +Both run on an NVIDIA GPU. -- **cuDF is not deprecated.** It remains a first-class, supported engine and is the right - choice for one very large materialization (footnote F3). -- **They execute differently.** ``cudf`` runs GFQL eagerly — each hop is a separate kernel - launch with a materialized intermediate. ``polars-gpu`` runs the **same fused lazy plan as - the CPU Polars engine**, collected once on the GPU. Fusing the plan is why ``polars-gpu`` - leads on heavy multi-hop and why even **CPU Polars often beats eager cuDF** on bulk work. -- **Frame type.** ``cudf`` operates on ``cudf.DataFrame``; ``polars-gpu`` operates on - ``polars.DataFrame`` (only the lazy ``.collect()`` runs on the GPU). Either way, a graph - built from pandas frames is accepted and coerced for you — only the keyword changes. -- **Install.** ``cudf`` and ``polars-gpu`` both need the RAPIDS GPU stack; ``polars-gpu`` - additionally uses ``cudf_polars``. ``polars`` (CPU) only needs ``pip install polars``. +- ``cudf`` runs the query one operation at a time; each hop is a separate kernel with a + materialized intermediate. It is a supported, first-class engine and the right choice + for one very large result. +- ``polars-gpu`` runs the same fused plan as CPU Polars, collected once on the GPU. That + is why it leads on heavy multi-hop work. +- ``cudf`` operates on ``cudf.DataFrame``; ``polars-gpu`` on ``polars.DataFrame``, with + only the collect running on the GPU. A graph built from pandas frames works with either. +- Both need the RAPIDS GPU stack; ``polars-gpu`` also uses ``cudf_polars``. CPU + ``polars`` needs only ``pip install polars``. .. _gfql-larger-than-memory: Larger-than-memory: streaming execution --------------------------------------- -The default Polars engines run **in-memory**: fastest and most stable while the -graph and its query intermediates fit in RAM (or device memory). When a query's -*intermediates* would blow past memory — a wide multi-hop frontier, a large -join, a big aggregation — GFQL has two **opt-in** streaming modes that trade a +The Polars engines run in memory by default, which is fastest while the graph and the +query's intermediate results fit in RAM or GPU memory. Two opt-in streaming modes trade a little latency for a much larger working set: .. list-table:: @@ -393,21 +283,14 @@ little latency for a much larger working set: - What it does * - ``GFQL_POLARS_CPU_STREAMING=1`` - ``polars`` - - Collects the fused plan with Polars' **streaming engine** — processes in - batches and **spills to disk**, so intermediates can exceed RAM. + - Runs the plan with Polars' streaming engine: batches, with spill to disk, so + intermediate results can exceed RAM. * - ``GFQL_POLARS_GPU_EXECUTOR=streaming`` - ``polars-gpu`` - - Uses the **cudf-polars streaming executor** — the escape hatch for - results **larger than device memory** (the default in-memory executor - would OOM). - -Both are **off by default** on purpose: they add overhead that mildly *regresses* -small/interactive work, and for the in-memory regime the benchmarks measure, the -default is faster and more stable. Results are -**parity-identical** to the default — streaming changes *how* the plan runs, not -*what* it returns. + - Uses the cudf-polars streaming executor for results larger than GPU memory. -Set them by environment variable: +Both are off by default because they slow down small, interactive work. Results are +identical to the default modes. Set them by environment variable: .. code-block:: bash @@ -417,8 +300,7 @@ Set them by environment variable: # GPU: streaming executor for larger-than-device-memory results export GFQL_POLARS_GPU_EXECUTOR=streaming -...or from Python at runtime — the setting is read **live** (per collect), and a Python -override takes precedence over the environment variable: +or from Python at runtime; a Python setting overrides the environment: .. doc-test: skip @@ -431,74 +313,27 @@ override takes precedence over the environment variable: set_cpu_streaming(True) # CPU streaming collect (pass None to reset to env/default) set_gpu_executor('streaming') # one of GPU_EXECUTORS == ('in-memory', 'streaming') -Then use ``engine='polars'`` / ``engine='polars-gpu'`` exactly as before — no code -change: - -.. doc-test: skip - -.. code-block:: python - - import graphistry # env vars above must be set first - g = graphistry.edges(edges_df, 'src', 'dst') - result = g.gfql(query, engine='polars') # streaming collect (CPU, disk-spill) - # result = g.gfql(query, engine='polars-gpu') # streaming executor (GPU) +Then call ``g.gfql(query, engine='polars')`` or ``engine='polars-gpu'`` as before. .. note:: - **What streaming does and does not cover today.** These flags stream the - **query** (collect), which helps when the *input fits but the intermediates or - result do not*. They do **not** yet give out-of-core *input*: ``graphistry`` - currently materializes edge/node frames at ingestion (a passed - ``polars.LazyFrame`` is collected immediately), so the source graph must still - fit in memory. True out-of-core-from-disk — building GFQL directly on a lazy - ``pl.scan_parquet`` source so a graph larger than RAM never fully materializes — - is **work in progress**; see the Friendster (~1.8B edges) discussion in the - GraphFrames benchmark page. - -When **not** to use Polars --------------------------- - -Three cases, stated so you can route around them: - -- **Trivially small operations** (a bare node-equality filter): pandas' boolean mask - beats Polars' plan overhead, and in absolute terms it is immaterial. For traversal / - ``WHERE`` / ``ORDER`` / aggregation, Polars leads on CPU (footnote F1). The real - small-size caveat is **GPU-only** (cuDF / Polars-GPU need larger work — footnote F2). -- **A few exotic Cypher features** are not yet native on Polars (e.g. cross-entity same-path - ``WHERE``, some temporal/entity-text forms). GFQL rejects those shapes during - validation, compilation, or planning before query execution and points at - ``engine='pandas'`` — it **never** silently bridges Polars to pandas, because that would - misreport pandas performance as Polars (see *Parity and fallback rules*). -- **One extreme materialization (a huge output row count):** prefer ``cudf`` over - ``polars-gpu`` (footnote F3). - -Parity and fallback rules -------------------------- - -- **Identical results across engines.** Differential parity — every engine's output must match - the pandas oracle — is a release gate, exercised across forward/reverse/undirected, 1-3 hop, - filters, and aggregations. -- **No silent fallback for traversal / filter / row ops — parity-verified.** For ``hop`` / - ``WHERE`` / ``RETURN`` / aggregation, the Polars engine runs natively or the query is - declined before execution during validation, compilation, or planning. For string GFQL / - Cypher queries, known unsupported syntax and unsupported lowering shapes are rejected by - the compiler/validator before execution starts; Python-built ASTs hit the same safety - boundary in the local planner before the unsupported engine path runs. GFQL never quietly - converts to pandas, so a *traversal* latency you measure is real work on the engine you - asked for. ``polars-gpu`` is **GPU-or-error**: if any step of the plan cannot run on the - GPU, the plan is rejected rather than silently running on CPU and labelling it a GPU result. -- **Whole-graph analytics are the one mode-gated exception.** ``umap`` / ``hypergraph`` / - ``compute_cugraph`` and friends have no Polars kernel; under ``call_mode='auto'`` (default) - they run off-engine and warn once (see - :ref:`Analytics under Polars `). This is *not* silent — it warns — and - ``call_mode='strict'`` restores strict parity-or-pre-execution-decline for benchmark - integrity, so a benchmarked run can guarantee no hidden modality switch. - -Methodology ------------ - -Hosts, datasets, warm-median protocol, cross-engine result validation, provenance, and -reproducers live with the numbers on the :doc:`performance` page. A figure that cannot be -traced to a committed benchmark artifact is not published. + These modes stream the query, which helps when the input fits in memory but the + intermediate or final results do not. The source graph must still fit in memory: + ``graphistry`` materializes edge and node frames at ingestion, and a + ``polars.LazyFrame`` is collected immediately. Building GFQL directly on a lazy + ``pl.scan_parquet`` source, so that a graph larger than RAM never fully materializes, + is work in progress; see the Friendster discussion on the GraphFrames benchmark page. + +Same results on every engine +---------------------------- + +- Every engine returns the same result as pandas. This is tested across forward, + reverse and undirected traversal, one to three hops, filters and aggregations. +- Traversal, filter and row operations never change engine silently. The Polars engine + runs them natively or raises before running, so a latency you measure is real work on + the engine you asked for. ``polars-gpu`` also raises if any step of the plan cannot run + on the GPU. +- Whole-graph analytics are the one exception, and they warn: see + :ref:`gfql-offengine-calls`. ``call_mode='strict'`` turns that warning into an error. Install ------- @@ -510,43 +345,15 @@ Install # 'cudf' and 'polars-gpu' require the NVIDIA RAPIDS stack (GPU); # 'polars-gpu' additionally uses cudf_polars. -Then change one keyword — your existing graph and query are unchanged: - -.. doc-test: skip - -.. code-block:: python - - import graphistry - g = graphistry.edges(df, 'src', 'dst') # your existing pandas, Polars, or cuDF graph - g.gfql("MATCH (a)-[e]->(b) RETURN b", engine='polars') # CPU columnar - g.gfql("MATCH (a)-[e]->(b) RETURN b", engine='polars-gpu') # same plan on GPU - -What auto does --------------- - -``auto`` prefers the native engine for your frames and keeps a safety net. A few exotic -Cypher features still require ``engine='pandas'``: the Polars engine **declines them before -execution** rather than silently bridging. Under ``auto`` that decline is caught and the -call is re-served on ``pandas`` (all-cuDF frames decline back to ``cudf``), so a query that -works today keeps working while everything the native engine does support stays native. - -Pass the engine explicitly when you would rather know: ``engine='polars'`` / -``'polars-gpu'`` raise ``NotImplementedError`` on a declined shape instead of falling back, -which is what you want in a benchmark or a pipeline that must not silently change engines. -``engine='polars-gpu'`` is additionally GPU-or-error and never quietly runs on CPU. - -Performance is rarely the downside — CPU Polars wins common graph queries past -small/interactive sizes; only trivially small operations favor pandas, immaterially. - .. note:: - Non-GFQL surfaces (layouts, plotting, featurization) still consume Polars frames as an - *input format* and compute in pandas, so ``auto`` coerces there. The native-under-auto - behavior described above is specific to GFQL query execution. + Layouts, plotting and featurization accept Polars frames as input and compute in + pandas. The engine behavior on this page applies to GFQL query execution. See also -------- -- :doc:`performance` — GFQL performance overview -- :doc:`benchmark_filter_pagerank` — end-to-end CPU/GPU vs Neo4j+GDS +- :doc:`performance` — measured results against graph databases +- :doc:`benchmark_filter_pagerank` — a Cypher + PageRank pipeline vs Neo4j + GDS +- :doc:`index_adjacency` — queries that start from known nodes - :doc:`/api/gfql/index` — GFQL API reference - :doc:`remote` — run GFQL on a remote GPU diff --git a/docs/source/gfql/perf/index.rst b/docs/source/gfql/perf/index.rst index b9351e1bed..4f887d9d0e 100644 --- a/docs/source/gfql/perf/index.rst +++ b/docs/source/gfql/perf/index.rst @@ -1,25 +1,51 @@ Performance and Benchmarks ========================== -How to make GFQL fast, and what it measures against other systems. +GFQL runs graph queries as dataframe operations, in your process, on CPU or GPU. These +pages show what that is worth against graph databases and Spark, and how to get the +speed on your own data. -Start with :doc:`../engines` to pick pandas, Polars, cuDF, or Polars-GPU. -:doc:`../performance` covers vectorization, GPU acceleration, and measured -engine comparisons. :doc:`../indexing` and :doc:`../index_adjacency` speed up -queries that start from known nodes (a watchlist, a session, a seed set). :doc:`../remote` runs the same queries on -a Graphistry server GPU. +See what it is worth +-------------------- -The :doc:`../benchmark_graphframes` page compares GFQL with Apache Spark -GraphFrames on one machine. The Start Here -:doc:`case study <../benchmark_filter_pagerank>` compares one Cypher + PageRank -pipeline with Neo4j + GDS. +Read in this order if you are deciding whether GFQL fits. + +1. :doc:`Speedup case study: Cypher + PageRank vs Neo4j + GDS <../benchmark_filter_pagerank>` + — one real pipeline, measured end to end on a 30M-edge graph, on CPU and GPU. +2. :doc:`Measured against graph databases <../performance>` — the q1–q9 Cypher board + against Kuzu, Memgraph and Neo4j, and the SNB point-query comparison, with the losses + shown next to the wins. +3. :doc:`GFQL vs Spark GraphFrames <../benchmark_graphframes>` — one machine against a + cluster framework on LiveJournal and Orkut. + +Get the speed on your data +-------------------------- + +Read these when you have a graph and want it to run faster. + +1. :doc:`Choose an engine <../engines>` — pandas, Polars, cuDF or Polars-GPU with one + keyword; which one for which work. +2. :doc:`Index for queries that start from known nodes <../index_adjacency>` — build the + adjacency index once; a traversal from a watchlist or a seed set then reads only those + nodes' neighborhoods. +3. :doc:`Indexing guide <../indexing>` — property and adjacency indexes, when they engage, + and what they cost. +4. :doc:`Run on a remote GPU <../remote>` — the same queries on a Graphistry server GPU. + +Reference +--------- + +- Methodology, hosts, datasets and provenance for every number: the *Provenance* section + of :doc:`../performance`. +- Streaming for results larger than memory: :ref:`gfql-larger-than-memory`. .. toctree:: :maxdepth: 1 + :hidden: - Choosing an Engine <../engines> - Measured Performance <../performance> - Indexing Guide <../indexing> - Adjacency Index for Lookups from Known Nodes <../index_adjacency> + ../performance + ../benchmark_graphframes + ../engines + ../index_adjacency + ../indexing ../remote - Benchmark: vs Spark GraphFrames <../benchmark_graphframes> From 777475d2adc31fdda322c98012ad037735db2bc4 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 7 Sep 2026 11:50:01 -0700 Subject: [PATCH 28/29] docs(gfql): jargon pass on the perf pages (queries from known nodes, run records, identical results) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HdYcPgiafGGutW9KfG3gg1 --- .../source/gfql/benchmark_filter_pagerank.rst | 8 +++---- docs/source/gfql/benchmark_graphframes.rst | 14 +++++------ docs/source/gfql/index_adjacency.rst | 13 +++++------ docs/source/gfql/indexing.rst | 23 +++++++++---------- docs/source/gfql/overview.rst | 2 +- docs/source/gfql/performance.rst | 20 ++++++++-------- 6 files changed, 39 insertions(+), 41 deletions(-) diff --git a/docs/source/gfql/benchmark_filter_pagerank.rst b/docs/source/gfql/benchmark_filter_pagerank.rst index 38ce2e315f..15b5833a51 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -91,7 +91,7 @@ The query does not change between engines: Intermediate graphs stay in Arrow, pandas, or cuDF memory in the same Python process. GFQL returns the same result on every engine or rejects the query before execution; -see :doc:`engines` for the parity rules. +see :doc:`engines`. .. _neo4j-analog: @@ -139,9 +139,9 @@ Method and limits ----------------- - **Workload**: one pipeline (filter, PageRank, filter) on two SNAP graphs. - Selected-node parity is measured (Jaccard, gate 0.95): on Twitter the GFQL + Selected-node agreement is measured (Jaccard, threshold 0.95): on Twitter the GFQL CPU arm matches Neo4j at 0.9999; on GPlus the GFQL CPU arm selects exactly - the locked lane's set; the GPlus GPU arm is at 0.91 against CPU and is + the reference run's set; the GPlus GPU arm is at 0.91 against CPU and is therefore diagnostic-only (see the caveats below). - **Timing**: warm runs after warm-up (2 warm-ups, 5 timed runs, median). The GFQL arms were measured at the release commit named in the Measurement block; the Twitter Neo4j arm @@ -157,7 +157,7 @@ Method and limits - **Comparable ratio**: the GPU-vs-CPU column compares the same GFQL query and the same profile, so that ratio is published. - **Scope**: for the four-engine CPU/GPU comparison and engine choice, see - :doc:`engines`. For seeded lookups, see :doc:`index_adjacency`. For the + :doc:`engines`. For queries from known nodes, see :doc:`index_adjacency`. For the Spark GraphFrames comparison, see :doc:`benchmark_graphframes`. .. _pagerank-provenance: diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index df1dde6081..f0ea75a787 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -11,8 +11,8 @@ Graphistry's open-source graph query language: Cypher and Python chains that run in-process on dataframes, with no database or cluster. GraphFrames is Spark's graph library, run here on ``local[*]``, a single-node JVM using all cores. The workload is four tasks on two SNAP graphs, LiveJournal and Orkut, with Friendster as the -larger-than-memory size measured last. Every number below renders from a -committed pyg-bench receipt; the Measurement block at the end names the runs, hosts, and +larger-than-memory size measured last. Every number below comes from a +recorded benchmark run; the Measurement block at the end names the runs, hosts, and commits. **Where it stands.** The single-server ceiling measured here is Friendster: @@ -54,7 +54,7 @@ and hop tasks with ``engine="polars"`` under the Polars CPU streaming collect, o ``engine="polars-gpu"`` under the cudf-polars streaming executor. PageRank re-binds an eager copy outside the timer and calls cuGraph on the GPU or igraph on the CPU. The streaming collect is not a tax: with the same commit and protocol the eager collect -matched it on filter and 2-hop and was slower on 1-hop (the receipts are named in the +matched it on filter and 2-hop and was slower on 1-hop (the runs are named in the Measurement block). Every cell is the median of 5 timed runs after 2 warmups, and every task returns the same result size on every system that ran it. Times are milliseconds unless marked; lower is better. @@ -156,7 +156,7 @@ ran (see :ref:`graphframes-friendster`). - not attempted - see :ref:`graphframes-friendster` -Result sizes agree across the systems that ran each task, as recorded in the receipts: +Result sizes agree across the systems that ran each task, as recorded in the run records: .. list-table:: :header-rows: 1 @@ -260,7 +260,7 @@ RAM, a direct cuDF read exceeds the unified pool, and a 90 GB Spark driver heap The harness binds from ``pl.scan_parquet`` and collects through GFQL's streaming paths (``GFQL_POLARS_CPU_STREAMING=1`` for the Polars streaming engine, ``GFQL_POLARS_GPU_EXECUTOR=streaming`` for the cudf-polars streaming executor), with a -peak-memory receipt at every size. On Friendster the CPU streaming run loaded the graph +peak-memory record at every size. On Friendster the CPU streaming run loaded the graph (scan plus degree pass in about 20 seconds, 55.0 GiB resident), answered the degree filter and the 1-hop from 50 hub seeds (table above), and peaked at 103.6 GiB resident after the 1-hop; a second run answered the 2-hop, a 15,878,312-node ball, in @@ -294,13 +294,13 @@ Method and limits Cells marked diagnostic are never quoted as GFQL's number. - **PageRank convergence**: GraphFrames runs a fixed ``maxIter=20``; cuGraph runs to its default tolerance. Times compare wall-clock to a usable ranking. -- **Receipts**: one run at a time under a host lock, after two clean checks five +- **Run records**: one run at a time under a host lock, after two clean checks five minutes apart; a load monitor samples the host every second and a classifier invalidates the run if a process outside the benchmark ran during it. Invalidated attempts stay in the package under ``stale-attempts/``; one Orkut GraphFrames run is valid by reclassification after the classifier learned that Spark's own shutdown cleanup is the benchmark's process (``RECLASSIFIED.txt`` in that run's directory). -- **Harness**: the GFQL streaming harness and every receipt live in pyg-bench; the +- **Harness**: the GFQL streaming harness and every run record live in pyg-bench; the GraphFrames baseline is ``benchmarks/gfql/bench_graphframes.py --systems graphframes`` in this repository, run from a host Spark with the GraphFrames assembly jar. diff --git a/docs/source/gfql/index_adjacency.rst b/docs/source/gfql/index_adjacency.rst index 2d7dde9a09..5e0fcc58ec 100644 --- a/docs/source/gfql/index_adjacency.rst +++ b/docs/source/gfql/index_adjacency.rst @@ -17,8 +17,8 @@ When to use it - **Seeded traversals**: you start from specific node ids (a watchlist, a session, a fraud ring's known members) and hop out 1–3 steps. -- **Repeated queries** against the same graph: build the index once, amortize it over many - seeded lookups. +- **Repeated queries** against the same graph: build the index once and reuse it over many + such queries. - **Interactive / point-lookup latency**: neighbor expansion whose cost tracks the seeds rather than the graph. @@ -117,9 +117,8 @@ kernel-launch floor dominates it and a CPU engine — pandas or Polars, both bac GPU pulls ahead (see :doc:`engines`). Pick the index for selective traversal and a **CPU engine** to drive it. -Latency figures for this lane are not published yet: it has not been run under the -provenance-carrying harness described on :doc:`performance`, and this page publishes -nothing it cannot trace to a committed artifact. Reproducers: +Latency figures for this path are not published yet: it has not been run under the +protocol described on :doc:`performance`. Reproducers: ``benchmarks/gfql/index_takeover_bench.py``, ``benchmarks/gfql/index_vs_dbs.py``, ``benchmarks/gfql/index_vs_kuzu_prepared.py``. @@ -131,7 +130,7 @@ Cost and fallback pay it back. - **No change to default behavior.** With no index resident and ``index_policy='use'`` (the default), queries run exactly as before. -- **Parity-or-fallback.** The index accelerates the seeded scan sites it covers (forward / +- **Same results, with or without the index.** The index accelerates the seeded scan sites it covers (forward / reverse hop, the Polars hop, the single-hop chain fast path). Any uncovered feature — edge / source / destination match, ``target_wave_front``, ``min_hops>1``, labeling — falls back to the scan/join path. The indexed subgraph is verified equal to the scan @@ -141,6 +140,6 @@ Cost and fallback See also -------- -- :doc:`engines` — choosing pandas / Polars / cuDF / Polars-GPU for non-seeded work. +- :doc:`engines` — choosing pandas / Polars / cuDF / Polars-GPU for queries that scan the graph. - :doc:`performance` — the vectorization + GPU design behind GFQL. - :doc:`benchmark_filter_pagerank` — an end-to-end filter → PageRank → filter comparison vs Neo4j. diff --git a/docs/source/gfql/indexing.rst b/docs/source/gfql/indexing.rst index 59895fe5b6..5232cd7985 100644 --- a/docs/source/gfql/indexing.rst +++ b/docs/source/gfql/indexing.rst @@ -16,7 +16,7 @@ For the planner policy knobs and competitive benchmarks, see .. code-block:: python g = g.gfql_index_all() # pay once ... - g.gfql(...) # ... every later seeded lookup on g rides the index + g.gfql(...) # ... every later query from known nodes uses the index What a resident index is ------------------------ @@ -49,7 +49,7 @@ carrying them: They are **sidecars over row positions**: your ``.edges`` / ``.nodes`` frames are never reordered or copied, and the resident footprint is visible per index via ``g.show_indexes()`` (the ``nbytes`` column). The model is **pay-as-you-go**: one -``O(E log E)`` build, amortized over every seeded query afterward. Nothing is built +``O(E log E)`` build, spread over every later query from known nodes. Nothing is built unless you ask. Quick start @@ -79,7 +79,7 @@ A complete, runnable example: g_indexed = g.gfql_index_all() print(g_indexed.show_indexes()[["name", "kind", "key_col", "n_keys", "valid"]]) - # Seeded 1-hop: who did accounts 0 and 3 transfer to? + # 1-hop from known nodes: who did accounts 0 and 3 transfer to? out = g_indexed.gfql([n({"id": is_in([0, 3])}), e_forward(), n()]) print(sorted(out._nodes["id"].tolist())) # [0, 1, 2, 3, 4] @@ -134,13 +134,13 @@ What uses the index today On 0.58.0, a resident index is consumed automatically by: -- **Seeded typed-hop fast paths** (native chain or Cypher): a seeded typed 1-hop — +- **Typed hops from known nodes** (native chain or Cypher): a typed 1-hop from known nodes — ``[n({"id": is_in([...])}), e_forward(), n(...)]`` or ``MATCH (m {id: $x})-[:T]->(p) RETURN p`` — including the single-alias **property RETURN** form (``RETURN p.a AS x, p.b``). The seed lookup, frontier expansion, and endpoint materialization all become positional index gathers, so the lookup stops paying graph-size costs. -- **Property-seeded lookups**: the seed filter may hit a *property* column (e.g. +- **Lookups by a property value**: the start filter may hit a *property* column (e.g. ``MATCH (m {id: $x})`` when the graph is bound on a different key column). The seed row falls back to a property scan, but the adjacency and endpoint gathers still engage — the common pattern of a synthetic key binding plus an ``id`` property @@ -184,7 +184,7 @@ time). Consequences: **Declines are always safe.** Whether an index is missing, stale, or the query shape is uncovered, results are identical either way — indexes only ever change speed, never -answers (index-vs-scan parity is differentially tested across engines). +answers. .. note:: **Stability.** The index kinds, sidecar layout, and ``show_indexes()`` columns describe @@ -208,18 +208,17 @@ What it costs, what it buys --------------------------- **Build (the "pay" side)**: one-time and ``O(E log E)`` — a sort over the edge frame, -amortized across every subsequent seeded query. ``index_policy='auto'`` only pays it when +spread across every later query from known nodes. ``index_policy='auto'`` only pays it when the planner predicts a selective query will earn it back. -**Seeded lookup (the "go" side)**: on a covered shape, the seeded lookup drops from the -general path to the fast path, and again with the index resident, on both CPU engines. +**Lookup (the "go" side)**: on a covered query, the lookup gets faster twice: once on +the specialized path and again once the index is built, on both CPU engines. -**Flat in graph size**: a direct seeded ``g.hop()`` with the index resident turns the +**Flat in graph size**: a direct ``g.hop()`` from known nodes with the index built turns the ``O(E)`` scan into an ``O(degree)`` gather, so its cost tracks the seeds' neighborhood rather than the graph. -Measured figures are published on :doc:`performance` and :doc:`index_adjacency` only, and -only when they trace to a committed benchmark artifact. +Measured figures are published on :doc:`performance` and :doc:`index_adjacency`. See also -------- diff --git a/docs/source/gfql/overview.rst b/docs/source/gfql/overview.rst index 8ee9d2d968..5c66c4abe8 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -34,7 +34,7 @@ GFQL is an in-process graph query language for the compute tier. With it you can On CPU alone, GFQL's Polars engine is faster than Kuzu, Memgraph, and Neo4j on most of the nine graph-benchmark analytics queries at both graph sizes, and the GPU engines are -faster still on heavy work; the graph databases win the SNB-derived point lookups. The +faster still on heavy work; the graph databases win the SNB-derived single-node lookups by id. The per-query boards, with every loss shown, are on :doc:`performance`, and a full filter, PageRank, filter pipeline against Neo4j + GDS is the :doc:`speedup case study `. diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 35f1af03b6..8c6ce37429 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -3,8 +3,8 @@ GFQL Performance: Measured Against Graph Databases ================================================== -This page holds GFQL's measured performance results. Every number renders from a -committed pyg-bench artifact; the Measurement block at the end names the runs, hosts, +This page holds GFQL's measured performance results. Every number comes from a +recorded benchmark run; the Provenance section at the end names the runs, hosts, and commits. Losses appear next to wins. Choose an engine @@ -50,7 +50,7 @@ At 20,000 people, GFQL Polars is faster than Kuzu on :bench-tally:`graphbench.100k|polars|kuzu` (Kuzu), :bench-tally:`graphbench.100k|polars|memgraph` (Memgraph), and :bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Kuzu wins q4 at 20,000 people and -q8 at 100,000 people; the artifact's compare tables classify both as ties because the +q8 at 100,000 people; the run's comparison tables classify both as ties because the per-slot medians overlap. Memgraph wins q3 and q6 at 20,000 people and q5, q6, and q7 at 100,000 people, where Neo4j also wins q5: their planners start from the ten-node interest side, which GFQL's Cypher path does not yet do. @@ -77,20 +77,20 @@ SNB-derived point and small-result queries: the databases win Matched query shapes derived from the LDBC Social Network Benchmark (SNB) Interactive workload, run on the SF0.1 and SF1 datasets without the official LDBC driver. This is internal evidence, not an official LDBC result. All four engines ran under one timing -contract with exact result parity. Times are milliseconds. +contract and returned identical results. Times are milliseconds. Kuzu, Neo4j, and Memgraph are faster than GFQL on every point-lookup row, and Memgraph is fastest on most. The GFQL columns run with resident indexes built once before the timed runs (``gfql_index_all`` plus node property indexes), the same footing as the databases' primary-key and label indexes, and the GFQL arm runs native op lists, not -Cypher text. A seeded lookup, a seeded typed hop, and a node-only lookup now resolve -through the resident node-id, adjacency, and node-property indexes on every CPU engine, +Cypher text. A lookup from a known node, a typed hop from it, and a node-only lookup now use +the node-id, adjacency, and node-property indexes on every CPU engine, so the SF0.1 point rows sit in the low single-digit milliseconds on pandas and under about ten milliseconds on polars, against a database's sub-millisecond index probe. The hop-shaped rows (message replies, recent replies, new topics) are unchanged by that work and remain GFQL's slowest cells here. GFQL's strengths are the bulk shapes above and on the :doc:`speedup case study `; choose a database when the -workload is dominated by point lookups. +workload is mostly single-node lookups by id. Open items behind the remaining gaps: CPU PageRank spends most of its time converting to igraph (`#2032 `_); a polars @@ -126,7 +126,7 @@ Lookups from known nodes A query that starts from known node ids (a watchlist, a session) scans every edge by default. The opt-in adjacency index turns that scan into a gather over the seeds' neighbors, so its cost tracks the seeds rather than the graph, on every engine. This -lane has not yet been measured under the provenance-carrying harness used above, so +path has not yet been measured under the protocol used above, so this page prints no figure for it; see :doc:`index_adjacency` for the design and :doc:`indexing` for the lifecycle. @@ -137,8 +137,8 @@ GFQL joins tables of nodes and edges in batches instead of following one path at time, over columnar frames based on `Apache Arrow `_. Polars fuses the operations into one lazy plan and collects once; cuDF and Polars GPU run the same columnar operations on NVIDIA GPUs. That favors bulk work: multi-join analytics, -frontier expansion from many seeds, and full-graph aggregation. It does not favor -single-row point lookups: the resident indexes bring a seeded lookup to a few +expansion from many starting nodes, and full-graph aggregation. It does not favor +single-node lookups by id: the indexes bring such a lookup to a few milliseconds, and an indexed database still answers in well under a millisecond, as the SNB tables show. From d660e33b0764ea5eb940a163681cb4ed40ace804 Mon Sep 17 00:00:00 2001 From: Leo Meyerovich Date: Mon, 7 Sep 2026 12:33:26 -0700 Subject: [PATCH 29/29] docs(gfql): benchmark numbers re-vendored from the master 1a4107924 re-measure (pyg-bench #251); board prose follows the new verdicts Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HdYcPgiafGGutW9KfG3gg1 --- docs/source/_data/gfql_benchmarks.json | 606 +++++++++++++------------ docs/source/gfql/performance.rst | 9 +- 2 files changed, 309 insertions(+), 306 deletions(-) diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 1ab629130d..27db53ac07 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -8,10 +8,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 152.67, + "value": 149.47, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.memgraph": { @@ -22,7 +22,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 655.5, @@ -36,7 +36,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 680.72, @@ -50,10 +50,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 163.57, + "value": 163.72, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars": { @@ -64,10 +64,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 26.38, + "value": 26.61, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars_gpu": { @@ -78,10 +78,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 18.85, + "value": 19.02, "workload": "graph-benchmark 100k q1" }, "graphbench.100k.q1.polars_gpu_vs_pandas": { @@ -95,10 +95,10 @@ "graphbench.100k.q1.polars_gpu", "graphbench.100k.q1.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 8.7, + "value": 8.6, "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q1.polars_vs_pandas": { @@ -112,7 +112,7 @@ "graphbench.100k.q1.polars", "graphbench.100k.q1.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 6.2, @@ -126,10 +126,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 265.01, + "value": 275.88, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.memgraph": { @@ -140,7 +140,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 468.94, @@ -154,7 +154,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 632.7, @@ -168,10 +168,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 168.17, + "value": 167.74, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars": { @@ -182,10 +182,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 39.95, + "value": 40.28, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars_gpu": { @@ -196,10 +196,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 29.25, + "value": 29.08, "workload": "graph-benchmark 100k q2" }, "graphbench.100k.q2.polars_gpu_vs_pandas": { @@ -213,7 +213,7 @@ "graphbench.100k.q2.polars_gpu", "graphbench.100k.q2.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 5.8, @@ -230,7 +230,7 @@ "graphbench.100k.q2.polars", "graphbench.100k.q2.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 4.2, @@ -244,10 +244,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 34.45, + "value": 34.4, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.memgraph": { @@ -258,7 +258,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 30.17, @@ -272,7 +272,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 51.14, @@ -286,10 +286,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 69.47, + "value": 69.65, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars": { @@ -300,10 +300,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 9.92, + "value": 10.06, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars_gpu": { @@ -314,10 +314,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 9.38, + "value": 9.62, "workload": "graph-benchmark 100k q3" }, "graphbench.100k.q3.polars_gpu_vs_pandas": { @@ -331,10 +331,10 @@ "graphbench.100k.q3.polars_gpu", "graphbench.100k.q3.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 7.4, + "value": 7.2, "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q3.polars_vs_pandas": { @@ -348,10 +348,10 @@ "graphbench.100k.q3.polars", "graphbench.100k.q3.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 7.0, + "value": 6.9, "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q4.kuzu": { @@ -362,10 +362,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 13.69, + "value": 13.15, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.memgraph": { @@ -376,7 +376,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 37.64, @@ -390,7 +390,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 63.76, @@ -404,10 +404,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 62.79, + "value": 62.39, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars": { @@ -418,10 +418,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 8.75, + "value": 8.84, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars_gpu": { @@ -432,10 +432,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 10.11, + "value": 9.99, "workload": "graph-benchmark 100k q4" }, "graphbench.100k.q4.polars_gpu_vs_pandas": { @@ -449,7 +449,7 @@ "graphbench.100k.q4.polars_gpu", "graphbench.100k.q4.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 6.2, @@ -466,10 +466,10 @@ "graphbench.100k.q4.polars", "graphbench.100k.q4.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 7.2, + "value": 7.1, "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q5.kuzu": { @@ -480,10 +480,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 12.79, + "value": 12.46, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.memgraph": { @@ -494,7 +494,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 3.95, @@ -508,7 +508,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 8.96, @@ -522,10 +522,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 400.64, + "value": 397.67, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars": { @@ -536,10 +536,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 11.37, + "value": 12.87, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars_gpu": { @@ -550,10 +550,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 13.52, + "value": 13.65, "workload": "graph-benchmark 100k q5" }, "graphbench.100k.q5.polars_gpu_vs_pandas": { @@ -567,10 +567,10 @@ "graphbench.100k.q5.polars_gpu", "graphbench.100k.q5.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 29.6, + "value": 29.1, "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q5.polars_vs_pandas": { @@ -584,10 +584,10 @@ "graphbench.100k.q5.polars", "graphbench.100k.q5.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 35.2, + "value": 30.9, "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q6.kuzu": { @@ -598,10 +598,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 21.49, + "value": 24.43, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.memgraph": { @@ -612,7 +612,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 5.14, @@ -626,7 +626,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 22.94, @@ -640,10 +640,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 400.74, + "value": 400.05, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars": { @@ -654,10 +654,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 14.04, + "value": 12.66, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars_gpu": { @@ -668,10 +668,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 12.37, + "value": 13.71, "workload": "graph-benchmark 100k q6" }, "graphbench.100k.q6.polars_gpu_vs_pandas": { @@ -685,10 +685,10 @@ "graphbench.100k.q6.polars_gpu", "graphbench.100k.q6.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 32.4, + "value": 29.2, "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q6.polars_vs_pandas": { @@ -702,10 +702,10 @@ "graphbench.100k.q6.polars", "graphbench.100k.q6.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 28.5, + "value": 31.6, "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q7.kuzu": { @@ -716,7 +716,7 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 9.81, @@ -730,7 +730,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 3.74, @@ -744,7 +744,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 140.18, @@ -758,10 +758,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 113.31, + "value": 112.47, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars": { @@ -772,10 +772,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.1, + "value": 6.51, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars_gpu": { @@ -786,10 +786,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 14.56, + "value": 12.97, "workload": "graph-benchmark 100k q7" }, "graphbench.100k.q7.polars_gpu_vs_pandas": { @@ -803,10 +803,10 @@ "graphbench.100k.q7.polars_gpu", "graphbench.100k.q7.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 7.8, + "value": 8.7, "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q7.polars_vs_pandas": { @@ -820,10 +820,10 @@ "graphbench.100k.q7.polars", "graphbench.100k.q7.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 16.0, + "value": 17.3, "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.100k.q8.kuzu": { @@ -834,10 +834,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 9.74, + "value": 9.79, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.memgraph": { @@ -848,7 +848,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 7316.46, @@ -862,7 +862,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 3766.74, @@ -876,10 +876,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 77.01, + "value": 75.99, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars": { @@ -890,10 +890,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 14.01, + "value": 13.78, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars_gpu": { @@ -904,10 +904,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 178.58, + "value": 176.81, "workload": "graph-benchmark 100k q8" }, "graphbench.100k.q8.polars_gpu_vs_pandas": { @@ -921,7 +921,7 @@ "graphbench.100k.q8.polars_gpu", "graphbench.100k.q8.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 0.4, @@ -938,7 +938,7 @@ "graphbench.100k.q8.polars", "graphbench.100k.q8.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 5.5, @@ -952,10 +952,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 82.85, + "value": 83.5, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.memgraph": { @@ -966,7 +966,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 6199.8, @@ -980,7 +980,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", "value": 4843.46, @@ -994,10 +994,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 139.95, + "value": 137.05, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars": { @@ -1008,10 +1008,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 34.26, + "value": 33.77, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars_gpu": { @@ -1022,10 +1022,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 23.64, + "value": 23.81, "workload": "graph-benchmark 100k q9" }, "graphbench.100k.q9.polars_gpu_vs_pandas": { @@ -1039,10 +1039,10 @@ "graphbench.100k.q9.polars_gpu", "graphbench.100k.q9.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", - "value": 5.9, + "value": 5.8, "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.100k.q9.polars_vs_pandas": { @@ -1056,7 +1056,7 @@ "graphbench.100k.q9.polars", "graphbench.100k.q9.pandas" ], - "run": "graphbench-q1q9-100k-master-20260905", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "x", "value": 4.1, @@ -1070,10 +1070,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 15.37, + "value": 15.39, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.memgraph": { @@ -1084,7 +1084,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 66.08, @@ -1098,7 +1098,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 74.5, @@ -1112,10 +1112,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 27.85, + "value": 27.78, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars": { @@ -1126,10 +1126,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.29, + "value": 7.32, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars_gpu": { @@ -1140,10 +1140,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 6.21, + "value": 6.3, "workload": "graph-benchmark 20k q1" }, "graphbench.20k.q1.polars_gpu_vs_pandas": { @@ -1157,10 +1157,10 @@ "graphbench.20k.q1.polars_gpu", "graphbench.20k.q1.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 4.5, + "value": 4.4, "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q1.polars_vs_pandas": { @@ -1174,7 +1174,7 @@ "graphbench.20k.q1.polars", "graphbench.20k.q1.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", "value": 3.8, @@ -1188,10 +1188,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 36.4, + "value": 41.28, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.memgraph": { @@ -1202,7 +1202,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 44.41, @@ -1216,7 +1216,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 67.74, @@ -1230,7 +1230,7 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 29.71, @@ -1244,10 +1244,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 11.3, + "value": 11.4, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars_gpu": { @@ -1258,10 +1258,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 10.59, + "value": 10.51, "workload": "graph-benchmark 20k q2" }, "graphbench.20k.q2.polars_gpu_vs_pandas": { @@ -1275,7 +1275,7 @@ "graphbench.20k.q2.polars_gpu", "graphbench.20k.q2.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", "value": 2.8, @@ -1292,7 +1292,7 @@ "graphbench.20k.q2.polars", "graphbench.20k.q2.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", "value": 2.6, @@ -1306,10 +1306,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 6.02, + "value": 5.86, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.memgraph": { @@ -1320,7 +1320,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 4.16, @@ -1334,7 +1334,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 8.31, @@ -1348,10 +1348,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 10.11, + "value": 10.56, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars": { @@ -1362,10 +1362,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 4.58, + "value": 4.59, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars_gpu": { @@ -1376,10 +1376,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 4.56, + "value": 4.54, "workload": "graph-benchmark 20k q3" }, "graphbench.20k.q3.polars_gpu_vs_pandas": { @@ -1393,10 +1393,10 @@ "graphbench.20k.q3.polars_gpu", "graphbench.20k.q3.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.2, + "value": 2.3, "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q3.polars_vs_pandas": { @@ -1410,10 +1410,10 @@ "graphbench.20k.q3.polars", "graphbench.20k.q3.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.2, + "value": 2.3, "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q4.kuzu": { @@ -1424,10 +1424,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 3.2, + "value": 3.41, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.memgraph": { @@ -1438,7 +1438,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 6.38, @@ -1452,7 +1452,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 11.54, @@ -1466,10 +1466,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 8.75, + "value": 9.05, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars": { @@ -1480,10 +1480,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 3.38, + "value": 3.35, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars_gpu": { @@ -1494,10 +1494,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 16.41, + "value": 4.53, "workload": "graph-benchmark 20k q4" }, "graphbench.20k.q4.polars_gpu_vs_pandas": { @@ -1511,10 +1511,10 @@ "graphbench.20k.q4.polars_gpu", "graphbench.20k.q4.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 0.5, + "value": 2.0, "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q4.polars_vs_pandas": { @@ -1528,10 +1528,10 @@ "graphbench.20k.q4.polars", "graphbench.20k.q4.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.6, + "value": 2.7, "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q5.kuzu": { @@ -1542,10 +1542,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 5.82, + "value": 6.33, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.memgraph": { @@ -1556,7 +1556,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 4.63, @@ -1570,7 +1570,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 18.33, @@ -1584,10 +1584,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 76.67, + "value": 79.44, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars": { @@ -1598,10 +1598,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 3.84, + "value": 3.95, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars_gpu": { @@ -1612,10 +1612,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.34, + "value": 7.41, "workload": "graph-benchmark 20k q5" }, "graphbench.20k.q5.polars_gpu_vs_pandas": { @@ -1629,10 +1629,10 @@ "graphbench.20k.q5.polars_gpu", "graphbench.20k.q5.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 10.4, + "value": 10.7, "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q5.polars_vs_pandas": { @@ -1646,10 +1646,10 @@ "graphbench.20k.q5.polars", "graphbench.20k.q5.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 20.0, + "value": 20.1, "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q6.kuzu": { @@ -1660,10 +1660,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 8.7, + "value": 8.38, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.memgraph": { @@ -1674,7 +1674,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 4.85, @@ -1688,7 +1688,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 20.99, @@ -1702,10 +1702,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 77.99, + "value": 80.48, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars": { @@ -1716,10 +1716,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 4.81, + "value": 4.85, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars_gpu": { @@ -1730,10 +1730,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.18, + "value": 7.13, "workload": "graph-benchmark 20k q6" }, "graphbench.20k.q6.polars_gpu_vs_pandas": { @@ -1747,10 +1747,10 @@ "graphbench.20k.q6.polars_gpu", "graphbench.20k.q6.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 10.9, + "value": 11.3, "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q6.polars_vs_pandas": { @@ -1764,10 +1764,10 @@ "graphbench.20k.q6.polars", "graphbench.20k.q6.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 16.2, + "value": 16.6, "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q7.kuzu": { @@ -1778,10 +1778,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 5.31, + "value": 4.99, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.memgraph": { @@ -1792,7 +1792,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 5.39, @@ -1806,7 +1806,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 15.14, @@ -1820,10 +1820,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 18.16, + "value": 19.04, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars": { @@ -1834,10 +1834,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 3.09, + "value": 3.15, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars_gpu": { @@ -1848,10 +1848,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 8.57, + "value": 7.56, "workload": "graph-benchmark 20k q7" }, "graphbench.20k.q7.polars_gpu_vs_pandas": { @@ -1865,10 +1865,10 @@ "graphbench.20k.q7.polars_gpu", "graphbench.20k.q7.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.1, + "value": 2.5, "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q7.polars_vs_pandas": { @@ -1882,10 +1882,10 @@ "graphbench.20k.q7.polars", "graphbench.20k.q7.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 5.9, + "value": 6.0, "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q8.kuzu": { @@ -1896,10 +1896,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 2.79, + "value": 2.8, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.memgraph": { @@ -1910,7 +1910,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 198.8, @@ -1924,7 +1924,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 198.71, @@ -1938,10 +1938,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.41, + "value": 8.0, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars": { @@ -1952,10 +1952,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 2.66, + "value": 2.55, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars_gpu": { @@ -1966,10 +1966,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 2.17, + "value": 2.53, "workload": "graph-benchmark 20k q8" }, "graphbench.20k.q8.polars_gpu_vs_pandas": { @@ -1983,10 +1983,10 @@ "graphbench.20k.q8.polars_gpu", "graphbench.20k.q8.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 3.4, + "value": 3.2, "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q8.polars_vs_pandas": { @@ -2000,10 +2000,10 @@ "graphbench.20k.q8.polars", "graphbench.20k.q8.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.8, + "value": 3.1, "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars vs GFQL-pandas" }, "graphbench.20k.q9.kuzu": { @@ -2014,10 +2014,10 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 10.69, + "value": 10.75, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.memgraph": { @@ -2028,7 +2028,7 @@ "disclosures": [], "engine": "memgraph", "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 124.91, @@ -2042,7 +2042,7 @@ "disclosures": [], "engine": "neo4j", "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", "value": 422.29, @@ -2056,10 +2056,10 @@ "disclosures": [], "engine": "pandas", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 16.2, + "value": 17.01, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars": { @@ -2070,10 +2070,10 @@ "disclosures": [], "engine": "polars", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.68, + "value": 7.88, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars_gpu": { @@ -2084,10 +2084,10 @@ "disclosures": [], "engine": "polars-gpu", "measurement_profile": "graphbench-cold-binding", - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "ms", - "value": 6.95, + "value": 6.7, "workload": "graph-benchmark 20k q9" }, "graphbench.20k.q9.polars_gpu_vs_pandas": { @@ -2101,10 +2101,10 @@ "graphbench.20k.q9.polars_gpu", "graphbench.20k.q9.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.3, + "value": 2.5, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, "graphbench.20k.q9.polars_vs_pandas": { @@ -2118,10 +2118,10 @@ "graphbench.20k.q9.polars", "graphbench.20k.q9.pandas" ], - "run": "graphbench-q1q9-20k-master-20260905", + "run": "graphbench-q1q9-20k-master-20260907", "status": "ok", "unit": "x", - "value": 2.1, + "value": 2.2, "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars vs GFQL-pandas" }, "graphframes.friendster.filter.gfql_polars": { @@ -3451,10 +3451,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 1.659, + "value": 1.664, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.gfql_polars": { @@ -3481,10 +3481,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 3.469, + "value": 3.239, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" }, "snb.sf01.message_content.kuzu": { @@ -3544,10 +3544,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 2.867, + "value": 2.329, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.gfql_polars": { @@ -3574,10 +3574,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 6.396, + "value": 6.26, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" }, "snb.sf01.message_creator.kuzu": { @@ -3637,10 +3637,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 13.99, + "value": 15.159, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.gfql_polars": { @@ -3667,10 +3667,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 13.138, + "value": 12.489, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" }, "snb.sf01.message_replies.kuzu": { @@ -3698,10 +3698,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 70.572, + "value": 68.848, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.gfql_polars": { @@ -3728,10 +3728,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 44.467, + "value": 48.141, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" }, "snb.sf01.new_topics.kuzu": { @@ -3759,10 +3759,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 47.675, + "value": 48.911, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.gfql_polars": { @@ -3789,10 +3789,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 35.613, + "value": 35.368, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" }, "snb.sf01.recent_replies.kuzu": { @@ -3852,10 +3852,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 3.921, + "value": 3.861, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.gfql_polars": { @@ -3882,10 +3882,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 11.997, + "value": 11.287, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" }, "snb.sf01.seed_lookup.kuzu": { @@ -3946,10 +3946,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "result_mismatch", "unit": "ms", - "value": 59.461, + "value": 59.43, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf01.tag_cooccurrence.gfql_polars_idx": { @@ -3962,10 +3962,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "result_mismatch", "unit": "ms", - "value": 31.665, + "value": 34.64, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" }, "snb.sf1.is3.single_query.polars": { @@ -4007,10 +4007,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 1.682, + "value": 1.677, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, "snb.sf1.message_content.gfql_polars": { @@ -4037,10 +4037,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 2.991, + "value": 2.914, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, "snb.sf1.message_content.kuzu": { @@ -4100,10 +4100,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 2.381, + "value": 2.408, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, "snb.sf1.message_creator.gfql_polars": { @@ -4130,10 +4130,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 5.897, + "value": 5.532, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, "snb.sf1.message_creator.kuzu": { @@ -4193,10 +4193,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 533.559, + "value": 572.933, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, "snb.sf1.new_topics.gfql_polars": { @@ -4223,10 +4223,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 162.322, + "value": 180.931, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, "snb.sf1.new_topics.kuzu": { @@ -4254,10 +4254,10 @@ ], "engine": "gfql_pandas_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 4.042, + "value": 4.138, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, "snb.sf1.seed_lookup.gfql_polars": { @@ -4284,10 +4284,10 @@ ], "engine": "gfql_polars_idx", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-master-5a6586f22-20260905", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 10.719, + "value": 11.128, "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, "snb.sf1.seed_lookup.kuzu": { @@ -4340,12 +4340,16 @@ } }, "contract_version": 3, - "generated_at": "2026-09-06T01:42:41Z", - "generated_by": "graphistry/pyg-bench c6c781e0 scripts/export_docs_numbers.py", + "generated_at": "2026-09-07T19:32:30Z", + "generated_by": "graphistry/pyg-bench df60306e scripts/export_docs_numbers.py", "policy": { "accepted_regressions": [], "drift_waivers": { + "filter-pagerank-059-20260904": "Measured at the 0.59.0-era heads (pygraphistry 3fb216dd); the 0.60 stack landed since (#2055 #2056 #2054 #2062) changes chain RESULT contracts (binding-name alias declines, duplicate-id collapse, internal-column scoping) and the route dispatch layout, not the hop/PageRank kernels these rungs time; its perf receipts (sentinel at every head, q1-q9 and SNB re-measured at master 1a4107924 on 2026-09-07) moved within run-to-run spread. The GraphFrames/Spark ladder and the GPlus/Twitter PageRank pipelines are multi-hour dgx campaigns not re-run in this close-out; re-measure scheduled for the 0.60 release board.", "filter-pagerank-gplus-locked-20260830": "Competitor-only cells: this run publishes the Neo4j + GDS GPlus pipeline time (pagerank.gplus.neo4j_gds), which does not depend on graphistry/compute; the GFQL GPlus arms are re-measured at 0.59.0 in filter-pagerank-059-20260904. Re-measure Neo4j only when its own version or the pipeline contract changes.", + "graphframes-ladder-059-hops-20260904": "Measured at the 0.59.0-era heads (pygraphistry 3fb216dd); the 0.60 stack landed since (#2055 #2056 #2054 #2062) changes chain RESULT contracts (binding-name alias declines, duplicate-id collapse, internal-column scoping) and the route dispatch layout, not the hop/PageRank kernels these rungs time; its perf receipts (sentinel at every head, q1-q9 and SNB re-measured at master 1a4107924 on 2026-09-07) moved within run-to-run spread. The GraphFrames/Spark ladder and the GPlus/Twitter PageRank pipelines are multi-hour dgx campaigns not re-run in this close-out; re-measure scheduled for the 0.60 release board.", + "graphframes-ladder-20260904": "Measured at the 0.59.0-era heads (pygraphistry e951e9a2c); the 0.60 stack landed since (#2055 #2056 #2054 #2062) changes chain RESULT contracts (binding-name alias declines, duplicate-id collapse, internal-column scoping) and the route dispatch layout, not the hop/PageRank kernels these rungs time; its perf receipts (sentinel at every head, q1-q9 and SNB re-measured at master 1a4107924 on 2026-09-07) moved within run-to-run spread. The GraphFrames/Spark ladder and the GPlus/Twitter PageRank pipelines are multi-hour dgx campaigns not re-run in this close-out; re-measure scheduled for the 0.60 release board.", + "snb-aligned-release-20260902": "Competitor arms (Kuzu, Memgraph, Neo4j) of the aligned SNB matrix: their cells do not depend on graphistry/compute. The run's GFQL polars arm (unindexed, pygraphistry 3fb216dd) is not quoted by the docs; the GFQL SNB cells the docs quote are the indexed arms re-measured at master 1a4107924 (snb-master-1a4107924-20260907).", "snb-is3-single-query-20260727": "Diagnostic-only probe (comparison_allowed false) at pygraphistry 233b64c8; kept for the IS3 single-query versus two-pass note, never quoted as a result." }, "literal_allowlist": {}, @@ -4399,31 +4403,31 @@ "row_validation": "All 12 selected-node sets had Jaccard 1.0 against the reference (gate 0.95); every slot passed typed result, exact run-contract, and load/self-spike validation; the committed aggregate exactly recomputes from all slots.", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13); GFQL Python 3.13.12, pandas 2.3.3, python-igraph 1.0.0; Neo4j 2026.02.2 + graph-data-science with Python client 3.12.3 on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-100k-master-20260905": { - "artifact": "results/graphbench-master-5a6586f22-100k-20260905 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-100k-master-20260907": { + "artifact": "results/graphbench-master-1a4107924-100k-20260907 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 100,000 persons / 2,417,738 FOLLOWS / 2,775,195 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-06", + "measured_at": "2026-09-07", "perf_lock_held": true, - "pyg_bench_commit": "5cf81ae", - "pygraphistry_commit": "5a6586f22", + "pyg_bench_commit": "56873798e", + "pygraphistry_commit": "1a4107924", "quiet_host": true, "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2, cudf-polars for polars-gpu; Kuzu 0.11.3 in a host venv on CPU; Memgraph 2.18.1 and Neo4j 5 community in Docker on the same host; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "graphbench-q1q9-20k-master-20260905": { - "artifact": "results/graphbench-master-5a6586f22-20k-20260905 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", + "graphbench-q1q9-20k-master-20260907": { + "artifact": "results/graphbench-master-1a4107924-20k-20260907 with results/graphbench-memgraph-20260812 and results/graphbench-neo4j-20260812", "competitor": "Kuzu, Memgraph, Neo4j", "competitor_version": "Kuzu 0.11.3 (embedded, CPU); Memgraph 2.18.1 (Docker); Neo4j 5 community (Docker)", "dataset": "prrao87 graph-benchmark, 20,000 persons / 199,939 FOLLOWS / 260,107 edges", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-06", + "measured_at": "2026-09-07", "perf_lock_held": true, - "pyg_bench_commit": "5cf81ae", - "pygraphistry_commit": "5a6586f22", + "pyg_bench_commit": "56873798e", + "pygraphistry_commit": "1a4107924", "quiet_host": true, "reps": "GFQL and Kuzu: 5 warmups then 51 timed runs per slot, 4 position-balanced slots per engine (K/G/GPU), slot median then median across slots; GFQL binds the graph cold in every timed run (bind time included); the polars-gpu slots run under dgx-guard/safe_run.sh. Memgraph and Neo4j: 4 slots each of 7 timed runs over Bolt on 2026-08-12, values matched to the GFQL board. GFQL/Kuzu slots re-measured at pygraphistry master (release close-out); Memgraph and Neo4j arms unchanged.", "row_validation": "every cell emits a canonical, column-order-independent row set; a GFQL cell is publishable only when its rows equal every competitor's on every slot", @@ -4489,16 +4493,16 @@ "row_validation": "920/920 rows, value-identical to the independently derived expected fixture in all 3 repetitions", "runtime": "RAPIDS 26.02 (NVIDIA nvcr.io/nvidia/rapidsai/base:26.02-cuda12-py3.13 image, Python 3.13), polars 1.35.2; benchmark image graphistry/test-rapids-official:26.02-gfql-polars" }, - "snb-master-5a6586f22-20260905": { - "artifact": "results/snb-master-5a6586f22 (per-engine r1..r3 receipts + aggregate.json built with scripts/aggregate_snb_arm_runs.py from the release runner's run directories at pygraphistry master)", + "snb-master-1a4107924-20260907": { + "artifact": "results/snb-master-1a4107924 (per-engine r1..r3 receipts + aggregate.json built with scripts/aggregate_snb_arm_runs.py from the release runner's run directories at pygraphistry master)", "competitor": "none in this run (the competitor columns are the 2026-09-02 release board's)", "competitor_version": "n/a", "dataset": "LDBC SNB Interactive SF0.1 and SF1, derived matched query shapes (the release board's parameters)", "host": "dgx-spark (NVIDIA GB10), 20 CPU", - "measured_at": "2026-09-05", + "measured_at": "2026-09-07", "perf_lock_held": true, - "pyg_bench_commit": "5cf81ae", - "pygraphistry_commit": "5a6586f22", + "pyg_bench_commit": "56873798e", + "pygraphistry_commit": "1a4107924", "quiet_host": true, "reps": "2 discarded warmups, 5 measured executions with full result materialization, 3 independent process runs; median of the three run medians", "row_validation": "exact result parity per query against the suite's expected rows in every run; a cell is comparable only when its cardinalities equal the release GFQL arm's for the same query", diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 8c6ce37429..89da119d16 100644 --- a/docs/source/gfql/performance.rst +++ b/docs/source/gfql/performance.rst @@ -49,11 +49,10 @@ At 20,000 people, GFQL Polars is faster than Kuzu on :bench-tally:`graphbench.20k|polars|neo4j`. At 100,000 people the counts are :bench-tally:`graphbench.100k|polars|kuzu` (Kuzu), :bench-tally:`graphbench.100k|polars|memgraph` (Memgraph), and -:bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Kuzu wins q4 at 20,000 people and -q8 at 100,000 people; the run's comparison tables classify both as ties because the -per-slot medians overlap. Memgraph wins q3 and q6 at 20,000 people and q5, q6, and q7 at -100,000 people, where Neo4j also wins q5: their planners start from the ten-node -interest side, which GFQL's Cypher path does not yet do. +:bench-tally:`graphbench.100k|polars|neo4j` (Neo4j). Kuzu ties GFQL on q4 and q8 at 20,000 people and on q5 at 100,000 +people, and wins q8 at 100,000 people, where the per-slot medians overlap. Memgraph ties +q3 and q6 at 20,000 people and wins q5, q6, and q7 at 100,000 people. Neo4j also wins q5 there. Their planners +start from the ten-node interest side, which GFQL's Cypher path does not yet do. The 20,000-person board ~~~~~~~~~~~~~~~~~~~~~~~