diff --git a/CHANGELOG.md b/CHANGELOG.md index a3304fa5b2..cd7bdce7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,18 @@ 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; 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 + +- **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. ## [0.59.0 - 2026-08-31] 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/.rstcheck.cfg b/docs/.rstcheck.cfg index 322035f4f4..a0e60ac1b5 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 = @@ -47,6 +48,7 @@ ignore_directives = include, graphviz, bench-provenance, + bench-board, bench-disclosures # Ignore common informational messages diff --git a/docs/source/_data/gfql_benchmarks.json b/docs/source/_data/gfql_benchmarks.json index 5d5cd50c0c..27db53ac07 100644 --- a/docs/source/_data/gfql_benchmarks.json +++ b/docs/source/_data/gfql_benchmarks.json @@ -8,209 +8,235 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 153.16, + "value": 149.47, "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q1.pandas": { + "graphbench.100k.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-100k-20260726", + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 199.43, + "value": 655.5, "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q1.polars": { + "graphbench.100k.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-100k-20260726", + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 42.14, + "value": 680.72, "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q2.kuzu": { + "graphbench.100k.q1.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 281.91, - "workload": "graph-benchmark 100k q2" + "value": 163.72, + "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q2.pandas": { + "graphbench.100k.q1.polars": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 208.88, - "workload": "graph-benchmark 100k q2" + "value": 26.61, + "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q2.polars": { + "graphbench.100k.q1.polars_gpu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 55.76, - "workload": "graph-benchmark 100k q2" + "value": 19.02, + "workload": "graph-benchmark 100k q1" }, - "graphbench.100k.q3.kuzu": { + "graphbench.100k.q1.polars_gpu_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q1.polars_gpu", + "graphbench.100k.q1.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 28.76, - "workload": "graph-benchmark 100k q3" + "unit": "x", + "value": 8.6, + "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, - "graphbench.100k.q3.pandas": { + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 6.2, + "workload": "graph-benchmark 100k q1 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.100k.q2.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 73.72, - "workload": "graph-benchmark 100k q3" + "value": 275.88, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q3.polars": { + "graphbench.100k.q2.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "memgraph", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 14.74, - "workload": "graph-benchmark 100k q3" + "value": 468.94, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q4.kuzu": { + "graphbench.100k.q2.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "neo4j", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 11.56, - "workload": "graph-benchmark 100k q4" + "value": 632.7, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q4.pandas": { + "graphbench.100k.q2.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 64.09, - "workload": "graph-benchmark 100k q4" + "value": 167.74, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q4.polars": { + "graphbench.100k.q2.polars": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 12.13, - "workload": "graph-benchmark 100k q4" + "value": 40.28, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q5.kuzu": { + "graphbench.100k.q2.polars_gpu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 12.88, - "workload": "graph-benchmark 100k q5" + "value": 29.08, + "workload": "graph-benchmark 100k q2" }, - "graphbench.100k.q5.pandas": { + "graphbench.100k.q2.polars_gpu_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q2.polars_gpu", + "graphbench.100k.q2.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 410.05, - "workload": "graph-benchmark 100k q5" + "unit": "x", + "value": 5.8, + "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, - "graphbench.100k.q5.polars": { + "graphbench.100k.q2.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q2.polars", + "graphbench.100k.q2.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 17.52, - "workload": "graph-benchmark 100k q5" + "unit": "x", + "value": 4.2, + "workload": "graph-benchmark 100k q2 \u2014 GFQL-polars vs GFQL-pandas" }, - "graphbench.100k.q6.kuzu": { + "graphbench.100k.q3.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", @@ -218,255 +244,235 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 20.73, - "workload": "graph-benchmark 100k q6" + "value": 34.4, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q6.pandas": { + "graphbench.100k.q3.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "memgraph", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 412.3, - "workload": "graph-benchmark 100k q6" + "value": 30.17, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q6.polars": { + "graphbench.100k.q3.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "neo4j", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 21.99, - "workload": "graph-benchmark 100k q6" + "value": 51.14, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q7.kuzu": { + "graphbench.100k.q3.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 8.85, - "workload": "graph-benchmark 100k q7" + "value": 69.65, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q7.pandas": { + "graphbench.100k.q3.polars": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 119.63, - "workload": "graph-benchmark 100k q7" + "value": 10.06, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q7.polars": { + "graphbench.100k.q3.polars_gpu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 16.12, - "workload": "graph-benchmark 100k q7" + "value": 9.62, + "workload": "graph-benchmark 100k q3" }, - "graphbench.100k.q8.kuzu": { + "graphbench.100k.q3.polars_gpu_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", - "status": "ok", - "unit": "ms", - "value": 9.76, - "workload": "graph-benchmark 100k q8" - }, - "graphbench.100k.q8.pandas": { - "board_quotable": false, - "comparison_allowed": false, - "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." - ], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", - "status": "ok", - "unit": "ms", - "value": 91.6, - "workload": "graph-benchmark 100k q8" - }, - "graphbench.100k.q8.polars": { - "board_quotable": false, - "comparison_allowed": false, - "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." + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q3.polars_gpu", + "graphbench.100k.q3.pandas" ], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 5.06, - "workload": "graph-benchmark 100k q8" + "unit": "x", + "value": 7.2, + "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, - "graphbench.100k.q9.kuzu": { + "graphbench.100k.q3.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-100k-20260726", + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q3.polars", + "graphbench.100k.q3.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 84.06, - "workload": "graph-benchmark 100k q9" + "unit": "x", + "value": 6.9, + "workload": "graph-benchmark 100k q3 \u2014 GFQL-polars vs GFQL-pandas" }, - "graphbench.100k.q9.pandas": { + "graphbench.100k.q4.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 212.0, - "workload": "graph-benchmark 100k q9" + "value": 13.15, + "workload": "graph-benchmark 100k q4" }, - "graphbench.100k.q9.polars": { + "graphbench.100k.q4.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "memgraph", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-100k-20260726", + "engine": "memgraph", + "measurement_profile": "graphbench-memgraph-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 66.61, - "workload": "graph-benchmark 100k q9" + "value": 37.64, + "workload": "graph-benchmark 100k q4" }, - "graphbench.20k.q1.kuzu": { + "graphbench.100k.q4.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", + "competitor": "neo4j", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "engine": "neo4j", + "measurement_profile": "graphbench-neo4j-bolt-text-keyed-plan-cache", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 14.51, - "workload": "graph-benchmark 20k q1" + "value": 63.76, + "workload": "graph-benchmark 100k q4" }, - "graphbench.20k.q1.pandas": { + "graphbench.100k.q4.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 35.46, - "workload": "graph-benchmark 20k q1" + "value": 62.39, + "workload": "graph-benchmark 100k q4" }, - "graphbench.20k.q1.polars": { + "graphbench.100k.q4.polars": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 13.13, - "workload": "graph-benchmark 20k q1" + "value": 8.84, + "workload": "graph-benchmark 100k q4" }, - "graphbench.20k.q2.kuzu": { + "graphbench.100k.q4.polars_gpu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 31.62, - "workload": "graph-benchmark 20k q2" + "value": 9.99, + "workload": "graph-benchmark 100k q4" }, - "graphbench.20k.q2.pandas": { + "graphbench.100k.q4.polars_gpu_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q4.polars_gpu", + "graphbench.100k.q4.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 38.05, - "workload": "graph-benchmark 20k q2" + "unit": "x", + "value": 6.2, + "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" }, - "graphbench.20k.q2.polars": { + "graphbench.100k.q4.polars_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q4.polars", + "graphbench.100k.q4.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", - "unit": "ms", - "value": 17.59, - "workload": "graph-benchmark 20k q2" + "unit": "x", + "value": 7.1, + "workload": "graph-benchmark 100k q4 \u2014 GFQL-polars vs GFQL-pandas" }, - "graphbench.20k.q3.kuzu": { + "graphbench.100k.q5.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", @@ -474,447 +480,3878 @@ "disclosures": [], "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 5.94, - "workload": "graph-benchmark 20k q3" + "value": 12.46, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q3.pandas": { + "graphbench.100k.q5.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-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 11.68, - "workload": "graph-benchmark 20k q3" + "value": 3.95, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q3.polars": { + "graphbench.100k.q5.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-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 7.66, - "workload": "graph-benchmark 20k q3" + "value": 8.96, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q4.kuzu": { + "graphbench.100k.q5.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 3.42, - "workload": "graph-benchmark 20k q4" + "value": 397.67, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q4.pandas": { + "graphbench.100k.q5.polars": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 9.45, - "workload": "graph-benchmark 20k q4" + "value": 12.87, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q4.polars": { + "graphbench.100k.q5.polars_gpu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 6.04, - "workload": "graph-benchmark 20k q4" + "value": 13.65, + "workload": "graph-benchmark 100k q5" }, - "graphbench.20k.q5.kuzu": { + "graphbench.100k.q5.polars_gpu_vs_pandas": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, + "decimals": 1, "disclosures": [], - "engine": "kuzu", + "engine": "polars-gpu", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q5.polars_gpu", + "graphbench.100k.q5.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "x", + "value": 29.1, + "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 30.9, + "workload": "graph-benchmark 100k q5 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.100k.q6.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 5.75, - "workload": "graph-benchmark 20k q5" + "value": 24.43, + "workload": "graph-benchmark 100k q6" }, - "graphbench.20k.q5.pandas": { + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 22.94, + "workload": "graph-benchmark 100k q6" + }, + "graphbench.100k.q6.pandas": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, "disclosures": [], "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", "status": "ok", "unit": "ms", - "value": 81.03, - "workload": "graph-benchmark 20k q5" + "value": 400.05, + "workload": "graph-benchmark 100k q6" }, - "graphbench.20k.q5.polars": { + "graphbench.100k.q6.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 12.66, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 13.71, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 29.2, + "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 31.6, + "workload": "graph-benchmark 100k q6 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.100k.q7.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 9.81, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 140.18, + "workload": "graph-benchmark 100k q7" + }, + "graphbench.100k.q7.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 112.47, + "workload": "graph-benchmark 100k q7" + }, + "graphbench.100k.q7.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 6.51, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 12.97, + "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-master-20260907", + "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, + "decimals": 1, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "operands": [ + "graphbench.100k.q7.polars", + "graphbench.100k.q7.pandas" + ], + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "x", + "value": 17.3, + "workload": "graph-benchmark 100k q7 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.100k.q8.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 9.79, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 3766.74, + "workload": "graph-benchmark 100k q8" + }, + "graphbench.100k.q8.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 75.99, + "workload": "graph-benchmark 100k q8" + }, + "graphbench.100k.q8.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 13.78, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 176.81, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 0.4, + "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 5.5, + "workload": "graph-benchmark 100k q8 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.100k.q9.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", "decimals": 2, - "disclosures": [], + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 83.5, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4843.46, + "workload": "graph-benchmark 100k q9" + }, + "graphbench.100k.q9.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 137.05, + "workload": "graph-benchmark 100k q9" + }, + "graphbench.100k.q9.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-100k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 33.77, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 23.81, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 5.8, + "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 4.1, + "workload": "graph-benchmark 100k q9 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q1.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 15.39, + "workload": "graph-benchmark 20k q1" + }, + "graphbench.20k.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-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 66.08, + "workload": "graph-benchmark 20k q1" + }, + "graphbench.20k.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-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 74.5, + "workload": "graph-benchmark 20k q1" + }, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 27.78, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 7.32, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 6.3, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 4.4, + "workload": "graph-benchmark 20k q1 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "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", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 41.28, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 67.74, + "workload": "graph-benchmark 20k q2" + }, + "graphbench.20k.q2.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 29.71, + "workload": "graph-benchmark 20k q2" + }, + "graphbench.20k.q2.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 11.4, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 10.51, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.8, + "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.6, + "workload": "graph-benchmark 20k q2 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q3.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 5.86, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 8.31, + "workload": "graph-benchmark 20k q3" + }, + "graphbench.20k.q3.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 10.56, + "workload": "graph-benchmark 20k q3" + }, + "graphbench.20k.q3.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4.59, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4.54, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.3, + "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.3, + "workload": "graph-benchmark 20k q3 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q4.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 3.41, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 11.54, + "workload": "graph-benchmark 20k q4" + }, + "graphbench.20k.q4.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 9.05, + "workload": "graph-benchmark 20k q4" + }, + "graphbench.20k.q4.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 3.35, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4.53, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.0, + "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.7, + "workload": "graph-benchmark 20k q4 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q5.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 6.33, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 18.33, + "workload": "graph-benchmark 20k q5" + }, + "graphbench.20k.q5.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 79.44, + "workload": "graph-benchmark 20k q5" + }, + "graphbench.20k.q5.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 3.95, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 7.41, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 10.7, + "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 20.1, + "workload": "graph-benchmark 20k q5 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q6.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 8.38, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 20.99, + "workload": "graph-benchmark 20k q6" + }, + "graphbench.20k.q6.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 80.48, + "workload": "graph-benchmark 20k q6" + }, + "graphbench.20k.q6.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4.85, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 7.13, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 11.3, + "workload": "graph-benchmark 20k q6 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "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, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 4.99, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 15.14, + "workload": "graph-benchmark 20k q7" + }, + "graphbench.20k.q7.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 19.04, + "workload": "graph-benchmark 20k q7" + }, + "graphbench.20k.q7.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 3.15, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 7.56, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.5, + "workload": "graph-benchmark 20k q7 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "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, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 2.8, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 198.71, + "workload": "graph-benchmark 20k q8" + }, + "graphbench.20k.q8.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 8.0, + "workload": "graph-benchmark 20k q8" + }, + "graphbench.20k.q8.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 2.55, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 2.53, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 3.2, + "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 3.1, + "workload": "graph-benchmark 20k q8 \u2014 GFQL-polars vs GFQL-pandas" + }, + "graphbench.20k.q9.kuzu": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "kuzu", + "measurement_profile": "graphbench-kuzu-execute-text-per-call", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 10.75, + "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-master-20260907", + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 422.29, + "workload": "graph-benchmark 20k q9" + }, + "graphbench.20k.q9.pandas": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "pandas", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 17.01, + "workload": "graph-benchmark 20k q9" + }, + "graphbench.20k.q9.polars": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "kuzu", + "decimals": 2, + "disclosures": [], + "engine": "polars", + "measurement_profile": "graphbench-cold-binding", + "run": "graphbench-q1q9-20k-master-20260907", + "status": "ok", + "unit": "ms", + "value": 7.88, + "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-master-20260907", + "status": "ok", + "unit": "ms", + "value": 6.7, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.5, + "workload": "graph-benchmark 20k q9 \u2014 GFQL-polars-gpu vs GFQL-pandas" + }, + "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-master-20260907", + "status": "ok", + "unit": "x", + "value": 2.2, + "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 (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 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", + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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.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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "run": "graphframes-ladder-20260904", + "status": "ok", + "unit": "ms", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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 (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 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", + "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.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 (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 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", + "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 (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 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", + "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, + "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 (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 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", + "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, + "decimals": 2, + "disclosures": [], + "engine": "GFQL CPU (pandas + igraph)", + "measurement_profile": "filter-pagerank-resident-in-process", + "run": "filter-pagerank-059-20260904", + "status": "ok", + "unit": "s", + "value": 34.11, + "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL CPU (pandas + igraph)" + }, + "pagerank.gplus.gfql_gpu": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 2, + "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-059-20260904", + "status": "ok", + "unit": "s", + "value": 2.47, + "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL GPU (cuDF + cuGraph)" + }, + "pagerank.gplus.neo4j_gds": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 2, + "disclosures": [ + "The Neo4j arm writes marker properties during both filter stages and rebuilds its GDS projection per timed iteration; the paired GFQL arm retains resident frames and writes nothing. Twelve position-balanced slots (six per arm) produced exact selected-node parity, but the measurement profiles differ, so this is a direct pipeline time and no GFQL-vs-Neo4j ratio is published." + ], + "engine": "Neo4j + GDS", + "measurement_profile": "filter-pagerank-server-roundtrip-projection-per-iteration", + "run": "filter-pagerank-gplus-locked-20260830", + "status": "ok", + "unit": "s", + "value": 354.47, + "workload": "filter -> PageRank -> filter, gplus \u2014 Neo4j + GDS" + }, + "pagerank.twitter.gfql_cpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 2, + "disclosures": [], + "engine": "GFQL CPU (pandas + igraph)", + "measurement_profile": "filter-pagerank-resident-in-process", + "run": "filter-pagerank-059-20260904", + "status": "ok", + "unit": "s", + "value": 2.62, + "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL CPU (pandas + igraph)" + }, + "pagerank.twitter.gfql_gpu": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 2, + "disclosures": [], + "engine": "GFQL GPU (cuDF + cuGraph)", + "measurement_profile": "filter-pagerank-resident-in-process", + "run": "filter-pagerank-059-20260904", + "status": "ok", + "unit": "s", + "value": 0.23, + "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph)" + }, + "pagerank.twitter.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.twitter.gfql_gpu", + "pagerank.twitter.gfql_cpu" + ], + "run": "filter-pagerank-059-20260904", + "status": "ok", + "unit": "x", + "value": 11.3, + "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph) vs GFQL CPU (pandas + igraph)" + }, + "pagerank.twitter.neo4j_gds": { + "board_quotable": true, + "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.", + "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-059-20260904", + "status": "ok", + "unit": "s", + "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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 1.664, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-content" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 3.239, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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_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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 2.329, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-creator" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 6.26, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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_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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 15.159, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 message-replies" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 12.489, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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_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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 68.848, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 new-topics" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 48.141, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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_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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 48.911, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 recent-replies" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 35.368, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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_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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 3.861, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 seed-lookup" + }, + "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.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-master-1a4107924-20260907", + "status": "ok", + "unit": "ms", + "value": 11.287, + "workload": "SNB-derived matched queries (GFQL, resident indexes) 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.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-master-1a4107924-20260907", + "status": "result_mismatch", + "unit": "ms", + "value": 59.43, + "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-master-1a4107924-20260907", + "status": "result_mismatch", + "unit": "ms", + "value": 34.64, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf0.1 tag-cooccurrence" + }, + "snb.sf1.is3.single_query.polars": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 2, + "disclosures": [ + "The A/B configuration this was measured under declares comparison_allowed: false, so it is a diagnostic against the previous GFQL form only, never a competitor result." + ], + "engine": "polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-is3-single-query-20260727", + "status": "ok", + "unit": "ms", + "value": 18.05, + "workload": "LDBC SNB IS3 (SF1), single-query form" + }, + "snb.sf1.is3.two_pass.polars": { + "board_quotable": false, + "comparison_allowed": false, + "decimals": 2, + "disclosures": [ + "Answered by a two-pass adapter workaround with a Python-side join inside the timed region; superseded, kept only as the baseline for the single-query improvement." + ], "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", - "status": "ok", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-is3-single-query-20260727", + "status": "partial", "unit": "ms", - "value": 8.9, - "workload": "graph-benchmark 20k q5" + "value": 75.22, + "workload": "LDBC SNB IS3 (SF1), superseded two-pass adapter form" }, - "graphbench.20k.q6.kuzu": { + "snb.sf1.message_content.gfql_pandas_idx": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "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-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 8.23, - "workload": "graph-benchmark 20k q6" + "value": 1.677, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, - "graphbench.20k.q6.pandas": { + "snb.sf1.message_content.gfql_polars": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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": 81.25, - "workload": "graph-benchmark 20k q6" + "value": 3.556, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, - "graphbench.20k.q6.polars": { + "snb.sf1.message_content.gfql_polars_idx": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 10.43, - "workload": "graph-benchmark 20k q6" + "value": 2.914, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-content" }, - "graphbench.20k.q7.kuzu": { + "snb.sf1.message_content.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", - "decimals": 2, - "disclosures": [], + "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": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", "status": "ok", "unit": "ms", - "value": 5.19, - "workload": "graph-benchmark 20k q7" + "value": 0.674, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, - "graphbench.20k.q7.pandas": { + "snb.sf1.message_content.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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": 19.97, - "workload": "graph-benchmark 20k q7" + "value": 0.293, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, - "graphbench.20k.q7.polars": { + "snb.sf1.message_content.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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": 10.16, - "workload": "graph-benchmark 20k q7" + "value": 2.435, + "workload": "LDBC SNB-derived aligned queries sf1 message-content" }, - "graphbench.20k.q8.kuzu": { + "snb.sf1.message_creator.gfql_pandas_idx": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "kuzu", - "measurement_profile": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "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-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 2.52, - "workload": "graph-benchmark 20k q8" + "value": 2.408, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, - "graphbench.20k.q8.pandas": { - "board_quotable": false, - "comparison_allowed": false, - "competitor": "kuzu", - "decimals": 2, + "snb.sf1.message_creator.gfql_polars": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, "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." + "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": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "gfql_polars", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", "status": "ok", "unit": "ms", - "value": 10.46, - "workload": "graph-benchmark 20k q8" + "value": 78.38, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, - "graphbench.20k.q8.polars": { - "board_quotable": false, - "comparison_allowed": false, - "competitor": "kuzu", - "decimals": 2, + "snb.sf1.message_creator.gfql_polars_idx": { + "board_quotable": true, + "comparison_allowed": true, + "decimals": 3, "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." + "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": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-master-1a4107924-20260907", "status": "ok", "unit": "ms", - "value": 2.07, - "workload": "graph-benchmark 20k q8" + "value": 5.532, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 message-creator" }, - "graphbench.20k.q9.kuzu": { + "snb.sf1.message_creator.kuzu": { "board_quotable": true, "comparison_allowed": true, "competitor": "kuzu", - "decimals": 2, - "disclosures": [], + "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": "graphbench-kuzu-execute-text-per-call", - "run": "graphbench-q1q9-20k-20260726", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", "status": "ok", "unit": "ms", - "value": 10.85, - "workload": "graph-benchmark 20k q9" + "value": 1.509, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, - "graphbench.20k.q9.pandas": { + "snb.sf1.message_creator.memgraph": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "pandas", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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": 28.29, - "workload": "graph-benchmark 20k q9" + "value": 0.308, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, - "graphbench.20k.q9.polars": { + "snb.sf1.message_creator.neo4j": { "board_quotable": true, "comparison_allowed": true, - "competitor": "kuzu", - "decimals": 2, - "disclosures": [], - "engine": "polars", - "measurement_profile": "graphbench-reused-binding", - "run": "graphbench-q1q9-20k-20260726", + "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": 13.83, - "workload": "graph-benchmark 20k q9" + "value": 1.671, + "workload": "LDBC SNB-derived aligned queries sf1 message-creator" }, - "pagerank.gplus.gfql_cpu": { + "snb.sf1.new_topics.gfql_pandas_idx": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, - "disclosures": [], - "engine": "GFQL CPU (pandas + igraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "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-master-1a4107924-20260907", "status": "ok", - "unit": "s", - "value": 32.1, - "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL CPU (pandas + igraph)" + "unit": "ms", + "value": 572.933, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, - "pagerank.gplus.gfql_gpu": { + "snb.sf1.new_topics.gfql_polars": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, - "disclosures": [], - "engine": "GFQL GPU (cuDF + cuGraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "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": "s", - "value": 2.42, - "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL GPU (cuDF + cuGraph)" + "unit": "ms", + "value": 201.442, + "workload": "LDBC SNB-derived aligned queries sf1 new-topics" }, - "pagerank.gplus.gfql_gpu_vs_gfql_cpu": { + "snb.sf1.new_topics.gfql_polars_idx": { "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" + "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)." ], - "run": "filter-pagerank-20260728", + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-master-1a4107924-20260907", "status": "ok", - "unit": "x", - "value": 13.3, - "workload": "filter -> PageRank -> filter, gplus \u2014 GFQL GPU (cuDF + cuGraph) vs GFQL CPU (pandas + igraph)" + "unit": "ms", + "value": 180.931, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 new-topics" }, - "pagerank.gplus.neo4j_gds": { + "snb.sf1.new_topics.kuzu": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, + "competitor": "kuzu", + "decimals": 3, "disclosures": [ - "The Neo4j arm writes marker properties during both filter stages and rebuilds its GDS projection per timed iteration; the paired GFQL arm retains resident frames and writes nothing. Twelve position-balanced slots (six per arm) produced exact selected-node parity, but the measurement profiles differ, so this is a direct pipeline time and no GFQL-vs-Neo4j ratio is published." + "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 + GDS", - "measurement_profile": "filter-pagerank-server-roundtrip-projection-per-iteration", - "run": "filter-pagerank-gplus-locked-20260830", + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", "status": "ok", - "unit": "s", - "value": 354.47, - "workload": "filter -> PageRank -> filter, gplus \u2014 Neo4j + GDS" + "unit": "ms", + "value": 223.715, + "workload": "LDBC SNB-derived aligned queries sf1 new-topics" }, - "pagerank.twitter.gfql_cpu": { + "snb.sf1.seed_lookup.gfql_pandas_idx": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, - "disclosures": [], - "engine": "GFQL CPU (pandas + igraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "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-master-1a4107924-20260907", "status": "ok", - "unit": "s", - "value": 1.58, - "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL CPU (pandas + igraph)" + "unit": "ms", + "value": 4.138, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, - "pagerank.twitter.gfql_gpu": { + "snb.sf1.seed_lookup.gfql_polars": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, - "disclosures": [], - "engine": "GFQL GPU (cuDF + cuGraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "run": "filter-pagerank-20260728", + "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": "s", - "value": 0.24, - "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph)" + "unit": "ms", + "value": 89.161, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" }, - "pagerank.twitter.gfql_gpu_vs_gfql_cpu": { + "snb.sf1.seed_lookup.gfql_polars_idx": { "board_quotable": true, "comparison_allowed": true, - "decimals": 1, - "disclosures": [], - "engine": "GFQL GPU (cuDF + cuGraph)", - "measurement_profile": "filter-pagerank-resident-in-process", - "operands": [ - "pagerank.twitter.gfql_gpu", - "pagerank.twitter.gfql_cpu" + "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)." ], - "run": "filter-pagerank-20260728", + "engine": "gfql_polars_idx", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-master-1a4107924-20260907", "status": "ok", - "unit": "x", - "value": 6.7, - "workload": "filter -> PageRank -> filter, twitter \u2014 GFQL GPU (cuDF + cuGraph) vs GFQL CPU (pandas + igraph)" + "unit": "ms", + "value": 11.128, + "workload": "SNB-derived matched queries (GFQL, resident indexes) sf1 seed-lookup" }, - "pagerank.twitter.neo4j_gds": { + "snb.sf1.seed_lookup.kuzu": { "board_quotable": true, "comparison_allowed": true, - "decimals": 2, + "competitor": "kuzu", + "decimals": 3, "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." + "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 + GDS", - "measurement_profile": "filter-pagerank-server-roundtrip-projection-per-iteration", - "run": "filter-pagerank-20260728", + "engine": "kuzu", + "measurement_profile": "snb-interactive-warm-repeated-query", + "run": "snb-aligned-release-20260902", "status": "ok", - "unit": "s", - "value": 11.72, - "workload": "filter -> PageRank -> filter, twitter \u2014 Neo4j + GDS" + "unit": "ms", + "value": 2.517, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" }, - "snb.sf1.is3.single_query.polars": { - "board_quotable": false, - "comparison_allowed": false, - "decimals": 2, + "snb.sf1.seed_lookup.memgraph": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "memgraph", + "decimals": 3, "disclosures": [ - "The A/B configuration this was measured under declares comparison_allowed: false, so it is a diagnostic against the previous GFQL form only, never a competitor result." + "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": "polars", + "engine": "memgraph", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-is3-single-query-20260727", + "run": "snb-aligned-release-20260902", "status": "ok", "unit": "ms", - "value": 18.05, - "workload": "LDBC SNB IS3 (SF1), single-query form" + "value": 0.523, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" }, - "snb.sf1.is3.two_pass.polars": { - "board_quotable": false, - "comparison_allowed": false, - "decimals": 2, + "snb.sf1.seed_lookup.neo4j": { + "board_quotable": true, + "comparison_allowed": true, + "competitor": "neo4j", + "decimals": 3, "disclosures": [ - "Answered by a two-pass adapter workaround with a Python-side join inside the timed region; superseded, kept only as the baseline for the single-query improvement." + "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": "polars", + "engine": "neo4j", "measurement_profile": "snb-interactive-warm-repeated-query", - "run": "snb-is3-single-query-20260727", - "status": "partial", + "run": "snb-aligned-release-20260902", + "status": "ok", "unit": "ms", - "value": 75.22, - "workload": "LDBC SNB IS3 (SF1), superseded two-pass adapter form" + "value": 2.884, + "workload": "LDBC SNB-derived aligned queries sf1 seed-lookup" } }, "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-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": {}, "managed_docs": [ "gfql/performance.rst", @@ -932,23 +4369,24 @@ "performance.rst" ], "max_age_days": 60, - "max_compute_commit_drift": 12 + "max_compute_commit_drift": 12, + "max_unexplained_regression_pct": 25 }, "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": "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" + "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", @@ -963,37 +4401,82 @@ "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", - "competitor": "Kuzu", - "competitor_version": "0.11.3 (embedded, CPU)", + "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-07-26", + "measured_at": "2026-09-07", "perf_lock_held": true, - "pyg_bench_commit": "47f94ba", - "pygraphistry_commit": "84be35fb", + "pyg_bench_commit": "56873798e", + "pygraphistry_commit": "1a4107924", "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" - }, - "graphbench-q1q9-20k-20260726": { - "artifact": "results/graphbench-matched-q1q9-20260726/gb20k", - "competitor": "Kuzu", - "competitor_version": "0.11.3 (embedded, CPU)", + "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-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-07-26", + "measured_at": "2026-09-07", "perf_lock_held": true, - "pyg_bench_commit": "47f94ba", - "pygraphistry_commit": "84be35fb", + "pyg_bench_commit": "56873798e", + "pygraphistry_commit": "1a4107924", "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" + "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" + }, + "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 (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": "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.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)", + "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": "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", + "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-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": "15a6f3a7", + "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 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}", @@ -1008,7 +4491,22 @@ "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" + }, + "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-07", + "perf_lock_held": true, + "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", + "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/_ext/gfql_bench.py b/docs/source/_ext/gfql_bench.py index 48c698d2a0..2d95e09abf 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 @@ -34,6 +34,8 @@ from gfql_bench_data import ( BenchDataError, + format_tally, + tally, JSONObject, State, audit_pages, @@ -78,12 +80,41 @@ 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 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 +128,137 @@ 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 + 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 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 - 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) + 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 [] - return [_admonition('Measurement', _fields(run, self.FIELDS))] + 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): @@ -123,26 +272,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: @@ -197,8 +387,10 @@ 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.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/_ext/gfql_bench_charts.py b/docs/source/_ext/gfql_bench_charts.py index 044959e051..be7802c223 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 the ``graphframes.*`` cells of the +same artifact, once pyg-bench publishes the ladder. + Regenerate after vendoring a new artifact:: python3 docs/source/_ext/gfql_bench_charts.py --write @@ -32,6 +35,11 @@ os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'gfql', '_static', 'filter_pagerank') +#: 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') + WIDTH = 720 PAD = 16 HEADER_H = 74 @@ -99,10 +107,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.', )), )) @@ -200,25 +208,230 @@ 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. +#: 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'), + ('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_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): + 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)), + ('friendster_tasks.svg', GFChart('friendster', 'Friendster: 65,608,366 nodes / 1,806,067,135 edges', 148)), +)) + + +def gf_cell_key(dataset: str, task: str, system: str) -> str: + return '{}.{}.{}.{}'.format(GF_PREFIX, dataset, task, system) + + +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_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) + 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 + + +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_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 _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(chart.title)), + '{}'.format(_esc(chart.title)), + ''.format(STYLE), + '', + ''.format(WIDTH, height), + '{}'.format(PAD, _esc(chart.title)), + '{}'.format(PAD, _esc(subtitle)), + ] + 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) + 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))) + 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) + 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') + 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_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))) + 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[*].') + 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' + + +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) + if gf_published(data): + for name in GF_CHARTS: + charts[name] = render_graphframes(name, data) + 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 +440,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 +448,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/_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/_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/_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/friendster_tasks.svg b/docs/source/gfql/_static/graphframes/friendster_tasks.svg new file mode 100644 index 0000000000..52fee27401 --- /dev/null +++ b/docs/source/gfql/_static/graphframes/friendster_tasks.svg @@ -0,0 +1,44 @@ + +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) + +164890.2 ms +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 new file mode 100644 index 0000000000..da91eced7e --- /dev/null +++ b/docs/source/gfql/_static/graphframes/livejournal_tasks.svg @@ -0,0 +1,55 @@ + +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 unless marked. Lower is better. +filter: degree >= 42 + +GFQL polars (CPU) + +26.1 ms3.1x faster than GraphFrames +GFQL polars-gpu (GPU) + +27.1 ms3.0x faster than GraphFrames +GraphFrames local[*] + +80.4 ms +1-hop from 50 seeds + +GFQL polars (CPU) + +219.7 ms6.6x faster than GraphFrames +GFQL polars-gpu (GPU) + +1159.6 ms1.3x faster than GraphFrames +GraphFrames local[*] + +1460.2 ms +2-hop from 50 seeds + +GFQL polars (CPU) + +6478.3 ms0.5x of GraphFrames’ speed (slower) +GFQL polars-gpu (GPU) + +7162.5 ms0.5x of GraphFrames’ speed (slower) +GraphFrames local[*] + +3475.8 ms +PageRank, full graph + +GFQL polars (CPU) + + +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 +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[*]. + 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..89dadca9e6 --- /dev/null +++ b/docs/source/gfql/_static/graphframes/orkut_tasks.svg @@ -0,0 +1,55 @@ + +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 unless marked. Lower is better. +filter: degree >= 162 + +GFQL polars (CPU) + +59.4 ms1.1x faster than GraphFrames +GFQL polars-gpu (GPU) + +62.6 ms1.1x faster than GraphFrames +GraphFrames local[*] + +66.1 ms +1-hop from 50 seeds + +GFQL polars (CPU) + +514.0 ms7.1x faster than GraphFrames +GFQL polars-gpu (GPU) + +4037.1 ms0.9x of GraphFrames’ speed (slower) +GraphFrames local[*] + +3632.2 ms +2-hop from 50 seeds + +GFQL polars (CPU) + +27348.3 ms0.4x of GraphFrames’ speed (slower) +GFQL polars-gpu (GPU) + +33852.9 ms0.3x of GraphFrames’ speed (slower) +GraphFrames local[*] + +10885.8 ms +PageRank, full graph + +GFQL polars (CPU) + + +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 +GraphFrames local[*] + +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/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 9e05965acd..15b5833a51 100644 --- a/docs/source/gfql/benchmark_filter_pagerank.rst +++ b/docs/source/gfql/benchmark_filter_pagerank.rst @@ -1,14 +1,25 @@ -GFQL Cypher Filter + PageRank Benchmark -======================================== +Speedup Case Study: Cypher + PageRank, GFQL vs Neo4j + GDS +=========================================================== .. image:: _static/gfql-mascot.png :alt: GFQL mascot :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. +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 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 @@ -27,23 +38,21 @@ with **Neo4j + GDS** for the same three-stage pipeline. * - **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) -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. +.. image:: _static/filter_pagerank/twitter_pipeline.svg + :alt: Twitter warm pipeline time: Neo4j + GDS 11.72 s, GFQL Cypher CPU 2.62 s, GFQL Cypher GPU 0.23 s -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/gplus_pipeline.svg + :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 ------------ -One ``g.gfql(...)`` call searches the graph, calculates PageRank, and searches -the result: +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 @@ -70,72 +79,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. +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`. .. _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 +125,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,51 +133,40 @@ 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. + 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 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 + 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 + Neo4j column is therefore a direct pipeline time, not an engine-primitive + 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 + :doc:`engines`. For queries from known nodes, see :doc:`index_adjacency`. For the + Spark GraphFrames comparison, see :doc:`benchmark_graphframes`. .. _pagerank-provenance: -Benchmark environment and provenance ------------------------------------- - -Every figure is printed from ``docs/source/_data/gfql_benchmarks.json`` (pyg-bench). - -.. bench-provenance:: filter-pagerank-20260728 +Provenance +---------- -.. bench-provenance:: filter-pagerank-gplus-locked-20260830 +Every figure on this page is printed from ``docs/source/_data/gfql_benchmarks.json``, +which pyg-bench publishes. -.. bench-disclosures:: +.. bench-provenance:: filter-pagerank-059-20260904 filter-pagerank-gplus-locked-20260830 + :disclosures: diff --git a/docs/source/gfql/benchmark_graphframes.rst b/docs/source/gfql/benchmark_graphframes.rst index 5d431ff89b..f0ea75a787 100644 --- a/docs/source/gfql/benchmark_graphframes.rst +++ b/docs/source/gfql/benchmark_graphframes.rst @@ -1,118 +1,215 @@ -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) ----------------------------------- +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 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: +1,806,067,135 edges bound from a lazy Polars scan, a degree filter in +: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 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 +: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, 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 `_ +(`#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 + :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 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, 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 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 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. + +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 CPU vs GraphFrames * - **filter** (degree >= 42) - - 2.1ms - - 2.4ms - - 90.4ms - - **~43x** + - :bench:`graphframes.lj.filter.gfql_polars` + - :bench:`graphframes.lj.filter.gfql_polars_gpu` + - :bench:`graphframes.lj.filter.graphframes` + - :bench:`graphframes.lj.filter.gfql_polars_vs_graphframes` + * - **1-hop** (50 seeds) + - :bench:`graphframes.lj.hop1.gfql_polars` + - :bench:`graphframes.lj.hop1.gfql_polars_gpu` + - :bench:`graphframes.lj.hop1.graphframes` + - :bench:`graphframes.lj.hop1.gfql_polars_vs_graphframes` + * - **2-hop** (50 seeds) + - :bench:`graphframes.lj.hop2.gfql_polars` + - :bench:`graphframes.lj.hop2.gfql_polars_gpu` + - :bench:`graphframes.lj.hop2.graphframes` + - :bench:`graphframes.lj.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) + * - **PageRank** (full graph) + - :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`; 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 +: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 +----- + +.. list-table:: + :header-rows: 1 + :widths: 24 19 19 19 19 + + * - Task + - GFQL polars (CPU) + - GFQL polars-gpu (GPU) + - GraphFrames (local[*]) + - GFQL CPU vs GraphFrames + * - **filter** (degree >= 162) + - :bench:`graphframes.orkut.filter.gfql_polars` + - :bench:`graphframes.orkut.filter.gfql_polars_gpu` + - :bench:`graphframes.orkut.filter.graphframes` + - :bench:`graphframes.orkut.filter.gfql_polars_vs_graphframes` * - **1-hop** (50 seeds) - - 236.8ms - - 191.4ms - - 1421.7ms - - **~7.4x** + - :bench:`graphframes.orkut.hop1.gfql_polars` + - :bench:`graphframes.orkut.hop1.gfql_polars_gpu` + - :bench:`graphframes.orkut.hop1.graphframes` + - :bench:`graphframes.orkut.hop1.gfql_polars_vs_graphframes` * - **2-hop** (50 seeds) - - 1669.3ms - - 1542.1ms - - 3583.3ms - - **~2.3x** + - :bench:`graphframes.orkut.hop2.gfql_polars` + - :bench:`graphframes.orkut.hop2.gfql_polars_gpu` + - :bench:`graphframes.orkut.hop2.graphframes` + - :bench:`graphframes.orkut.hop2.gfql_polars_vs_graphframes` (GraphFrames wins) * - **PageRank** (full graph) - - 49.3s - - **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. + - :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`; CPU: :bench:`graphframes.orkut.pagerank.gfql_polars_vs_graphframes` (GraphFrames wins) + +Friendster +---------- + +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 + :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** (50 seeds) + - :bench:`graphframes.friendster.hop2.gfql_polars` + - 15,878,312 nodes + * - **PageRank** + - not attempted + - see :ref:`graphframes-friendster` + +Result sizes agree across the systems that ran each task, as recorded in the run records: + +.. list-table:: + :header-rows: 1 + :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 + * - Friendster (GFQL only) + - 6,585,312 + - 166,615 + - 15,878,312 + - not attempted + +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 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 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 +--------- + +**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 @@ -126,15 +223,8 @@ threshold column. # 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. +**1-hop** and **2-hop**: undirected expansion from a fixed set of 50 high-degree seed +nodes. .. doc-test: skip @@ -144,270 +234,91 @@ Undirected 1-hop expansion from a fixed 50-node high-degree seed set. 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 - - * - 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) -------------------- - -.. list-table:: - :header-rows: 1 - :widths: 26 18 18 18 20 - - * - Task - - GFQL polars (CPU) - - GFQL polars-gpu (GPU) - - GraphFrames (local[*]) - - Best GFQL vs GraphFrames - * - **filter** (degree >= 162) - - 1.7ms - - 2.0ms - - 70.6ms - - **~42x** - * - **1-hop** (50 seeds) - - 562.9ms - - 442.0ms - - 3826.6ms - - **~8.7x** - * - **2-hop** (50 seeds) - - 9439.8ms - - 8860.2ms - - 11582.9ms - - **~1.3x** - * - **PageRank** (full graph) - - 160.1s - - **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. - -.. 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``): - -.. 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``. 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 +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 GPU calls +``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: + +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 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 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 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 +: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 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: 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. + +.. _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. +- **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)``. +- **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. +- **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 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. + +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 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:`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/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..582a139c3e 100644 --- a/docs/source/gfql/engines.rst +++ b/docs/source/gfql/engines.rst @@ -3,24 +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. +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. -.. 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). - -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 @@ -30,40 +22,40 @@ 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: -.. 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: +.. doc-test: skip - .. code-block:: python +.. code-block:: python - 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 + out = g.gfql(query, engine='polars') # or 'cudf' / 'polars-gpu' + nodes_pd = out._nodes.to_pandas() # pandas for matplotlib, scikit-learn, .iloc, ... -.. 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. +**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 + +.. code-block:: python + + import polars as pl, graphistry + g = graphistry.edges(edges_pl, 'src', 'dst').nodes(nodes_pl, 'id') # polars frames + 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 ---------------- @@ -81,122 +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`. - -.. _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. + - The Polars fused plan run on the GPU (cudf_polars); fastest on heavy multi-hop work. -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. +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. -.. list-table:: - :header-rows: 1 - :widths: 18 40 42 - - * - Coming from - - Written in Cypher today? - - What GFQL gives you - * - **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`. - * - **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. - * - **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. - * - **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. - * - **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. - * - **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. - -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 @@ -205,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 *Honesty* 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 @@ -362,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:: @@ -402,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 @@ -426,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 @@ -440,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 *Honesty*). -- **One extreme materialization (a huge output row count):** prefer ``cudf`` over - ``polars-gpu`` (footnote F3). - -Parity and honesty ------------------- - -- **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 ------- @@ -519,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/index.rst b/docs/source/gfql/index.rst index 68b4f044d2..aef1b1e817 100644 --- a/docs/source/gfql/index.rst +++ b/docs/source/gfql/index.rst @@ -1,76 +1,43 @@ 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), :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 + lookups from known nodes 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 + Speedup Case Study: vs Neo4j + +.. toctree:: + :maxdepth: 1 + :caption: Guides + + Performance and Benchmarks + Language Reference .. toctree:: :maxdepth: 2 diff --git a/docs/source/gfql/index_adjacency.rst b/docs/source/gfql/index_adjacency.rst index 3574e575a4..5e0fcc58ec 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. @@ -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,21 +117,20 @@ 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``. -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 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 @@ Honesty and cost 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 39c18fbc8a..5232cd7985 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,14 +9,14 @@ 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 .. 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] @@ -127,20 +127,20 @@ 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 ------------------------- 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,23 +208,22 @@ 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 -------- -- :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 9dd893260e..5c66c4abe8 100644 --- a/docs/source/gfql/overview.rst +++ b/docs/source/gfql/overview.rst @@ -9,32 +9,44 @@ 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: - -- **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. -- **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. -- **Seamless 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. +GFQL is an in-process graph query language for the compute tier. With it you can: + +- **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. + +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 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 +`. + +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 ~~~~~~~~~~~~~~~~~~~ -.. toctree:: - :hidden: GFQL is built into pygraphistry: @@ -51,14 +63,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 @@ -313,7 +325,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 new file mode 100644 index 0000000000..4f887d9d0e --- /dev/null +++ b/docs/source/gfql/perf/index.rst @@ -0,0 +1,51 @@ +Performance and Benchmarks +========================== + +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. + +See what it is worth +-------------------- + +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: + + ../performance + ../benchmark_graphframes + ../engines + ../index_adjacency + ../indexing + ../remote diff --git a/docs/source/gfql/performance.rst b/docs/source/gfql/performance.rst index 6bf07bb856..89da119d16 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 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 ---------------- 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``, 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. .. doc-test: skip @@ -21,378 +24,147 @@ 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 ------------------------------------ - -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. - -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. - -See :doc:`benchmark_filter_pagerank` for the Neo4j filter → PageRank → filter -comparison. +The q1–q9 board: GFQL, Kuzu, Memgraph, and Neo4j +------------------------------------------------- + +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. + +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 +: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). 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 ~~~~~~~~~~~~~~~~~~~~~~~ -The q8 GFQL cells are diagnostic; all other cells are benchmark results. - -.. list-table:: - :header-rows: 1 - :widths: 10 12 20 20 20 - - * - Query - - Result rows - - Kuzu - - GFQL ``pandas`` - - GFQL ``polars`` - * - q1 - - 3 - - :bench:`graphbench.20k.q1.kuzu` - - :bench:`graphbench.20k.q1.pandas` - - :bench:`graphbench.20k.q1.polars` - * - q2 - - 1 - - :bench:`graphbench.20k.q2.kuzu` - - :bench:`graphbench.20k.q2.pandas` - - :bench:`graphbench.20k.q2.polars` - * - q3 - - 5 - - :bench:`graphbench.20k.q3.kuzu` - - :bench:`graphbench.20k.q3.pandas` - - :bench:`graphbench.20k.q3.polars` - * - q4 - - 2 - - :bench:`graphbench.20k.q4.kuzu` - - :bench:`graphbench.20k.q4.pandas` - - :bench:`graphbench.20k.q4.polars` - * - q5 - - 1 - - :bench:`graphbench.20k.q5.kuzu` - - :bench:`graphbench.20k.q5.pandas` - - :bench:`graphbench.20k.q5.polars` - * - q6 - - 5 - - :bench:`graphbench.20k.q6.kuzu` - - :bench:`graphbench.20k.q6.pandas` - - :bench:`graphbench.20k.q6.polars` - * - q7 - - 1 - - :bench:`graphbench.20k.q7.kuzu` - - :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` - * - q9 - - 1 - - :bench:`graphbench.20k.q9.kuzu` - - :bench:`graphbench.20k.q9.pandas` - - :bench:`graphbench.20k.q9.polars` +.. 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 ~~~~~~~~~~~~~~~~~~~~~~~~ -The same queries run on a graph with 100,000 people. - -.. list-table:: - :header-rows: 1 - :widths: 10 12 20 20 20 - - * - Query - - Result rows - - Kuzu - - GFQL ``pandas`` - - GFQL ``polars`` - * - q1 - - 3 - - :bench:`graphbench.100k.q1.kuzu` - - :bench:`graphbench.100k.q1.pandas` - - :bench:`graphbench.100k.q1.polars` - * - q2 - - 1 - - :bench:`graphbench.100k.q2.kuzu` - - :bench:`graphbench.100k.q2.pandas` - - :bench:`graphbench.100k.q2.polars` - * - q3 - - 5 - - :bench:`graphbench.100k.q3.kuzu` - - :bench:`graphbench.100k.q3.pandas` - - :bench:`graphbench.100k.q3.polars` - * - q4 - - 3 - - :bench:`graphbench.100k.q4.kuzu` - - :bench:`graphbench.100k.q4.pandas` - - :bench:`graphbench.100k.q4.polars` - * - q5 - - 1 - - :bench:`graphbench.100k.q5.kuzu` - - :bench:`graphbench.100k.q5.pandas` - - :bench:`graphbench.100k.q5.polars` - * - q6 - - 5 - - :bench:`graphbench.100k.q6.kuzu` - - :bench:`graphbench.100k.q6.pandas` - - :bench:`graphbench.100k.q6.polars` - * - q7 - - 1 - - :bench:`graphbench.100k.q7.kuzu` - - :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` - * - q9 - - 1 - - :bench:`graphbench.100k.q9.kuzu` - - :bench:`graphbench.100k.q9.pandas` - - :bench:`graphbench.100k.q9.polars` - -Provenance -~~~~~~~~~~ - -.. bench-provenance:: graphbench-q1q9-20k-20260726 - -.. bench-provenance:: graphbench-q1q9-100k-20260726 - -.. bench-disclosures:: - -The values come from committed `pyg-bench `_ -artifacts. The documentation build and ``docs/test_bench_numbers.py`` reject missing, -stale, or unpublished values. - -.. _gfql-bulk-sweep: - -Bulk engine comparison ----------------------- - -These measurements predate version 0.58.0. They use the SNAP -**com-LiveJournal** (35M edges) and **com-Orkut** (117M edges) graphs. - -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. - -.. 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 ----------------- +.. 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: + +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 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 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 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 +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 and SF1 GFQL +cells are the release measurement on landed master; the Measurement block below carries the +provenance. + +SF0.1 +~~~~~ + +.. bench-board:: snb.sf01 + :rows: seed_lookup,message_content,message_creator,recent_replies,message_replies,new_topics + :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 +~~~ + +.. bench-board:: snb.sf1 + :rows: seed_lookup,message_content,message_creator,new_topics + :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 +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 +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. + +How GFQL is fast, and when it is not +------------------------------------ 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. - -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. - -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. +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, +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. + +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-master-20260905 graphbench-q1q9-100k-master-20260905 snb-aligned-release-20260902 snb-master-5a6586f22-20260905 + :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/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 new file mode 100644 index 0000000000..46aa6f12f9 --- /dev/null +++ b/docs/source/gfql/reference/index.rst @@ -0,0 +1,28 @@ +Language Reference +================== + +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 language +specification, wire protocol, and embedding contracts, see +:doc:`../spec/index`. + +.. toctree:: + :maxdepth: 1 + + ../quick + ../cypher + ../where + ../return + ../predicates/quick + ../datetime_filtering + ../builtin_calls + ../combo + ../translate + ../policy + ../strict_mode + ../schema + ../wire_protocol_examples + ../loading_graph_data 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 diff --git a/docs/test_bench_numbers.py b/docs/test_bench_numbers.py index 826f43178c..1260881787 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 @@ -345,6 +345,83 @@ 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_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 + 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() @@ -360,3 +437,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}"