Skip to content

perf(gfql): fuse the polars single-hop grouped aggregate into one lazy plan - #1823

Merged
lmeyerov merged 2 commits into
masterfrom
perf/gfql-h2-grouped-agg-fused
Jul 28, 2026
Merged

perf(gfql): fuse the polars single-hop grouped aggregate into one lazy plan#1823
lmeyerov merged 2 commits into
masterfrom
perf/gfql-h2-grouped-agg-fused

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

_execute_single_hop_grouped_aggregate_fast_path serves the cypher shape

MATCH (a {..})-[{..}]->(b {..}) [WHERE ..]
RETURN <alias>.<prop> AS k [, ..], <agg> AS v [, ..] [ORDER BY ..] [LIMIT n]

which is three of the nine matched graph-benchmark cells — q1, q3 and q4. Its polars branch
chained ~7 eager ops — two semi-joins, two property joins, a group_by, a sort and a head,
each its own lazy().collect(_eager=True) — so every intermediate materialized, the
select([src, dst]) projection could not be pushed into the semi-joins, and the head() could not
reach into the plan at all. 74–96% of each of those queries' wall time sat inside those collects.

The same op sequence is now built as one lazy plan collected once. The algebra is
character-identical to the eager twin (same .unique() id frames, same semi-joins, same
un-deduplicated property lookups, same group_by(maintain_order=True).agg(..), same per-key
nulls_last sort, same head), so the value is identical — row order included.

Strictly additive: the eager code is untouched and is the fallback on every decline, so the
blast radius is a decline away from zero.


Sizing was rebuilt before anything was built, and it reproduced

Query text loaded from benchmarks/graphbench/matched_q1_q9/gb_queries.py, md5
6e7ae268a5a41742587fcb87854b6e27, verified against bench commit 7e5ce3b and unchanged at
pyg-bench 9311202. Nothing hand-written.

Baseline reconciliation vs the published 20k board — MATCHES. Clean master 49db91cc9, the
board's own runner gb_gfql.py, 4 slots:

arm vs published board
pandas (single-threaded; the control that shows tree/data/query/harness match) +0.8% … +9.4%, mean +4.9%
polars −17.6% … +16.3%, mean −0.9% (no systematic offset — quieter box than the last sizing round)
same-session Kuzu, 4 slots q1 −2.2%, q3 −0.7%, q4 +8.2%, q8 +0.1%, q9 +1.5%

In-path paired control (eager op chain vs one lazy plan, same process, 9 paired invocations per
query, values identical every time): 20k q1 −42.5%/−41.6%, q3 −42.4%/−36.7%, q4 −15.8%/−15.7%;
100k q1 −36.3%/−33.5%, q3 −29.8%/−23.3%, q4 −37.1%/−25.9%. The prior hand-built −35% figure for q3
reproduces from canonical text, so the build was worth doing.


Result: one win widened, one loss converted to a TIE, one loss narrowed. Zero flips to WIN.

dgx-spark, matched q1–q9 lane, one perf lock per experiment, master tree vs PR tree
position-balanced M P P M P M M P, per-slot medians (never best-of), and replicated end to end
in a second independent run
with 4 Kuzu slots instead of 2. Verdict rule fixed before the
replication ran: per-slot medians; overlapping ranges are a TIE, never a win.

20k (run 1 / run 2):

q master PR delta slot ranges vs same-session Kuzu
q1 12.80 / 13.31 8.71 / 8.96 −31.9% / −32.7% disjoint, both runs WIN 1.12–1.18× → WIN 1.66–1.73×
q3 8.66 / 8.54 5.58 / 5.53 −35.5% / −35.3% disjoint, both runs LOSE 1.36–1.37× → TIE
q4 6.35 / 6.99 4.99 / 5.05 −21.5% / −27.8% disjoint, both runs LOSE 1.90–2.10× → LOSE 1.49–1.51×
q5 7.25 / 7.54 7.88 / 6.87 +8.7% / −8.9% overlapping, both TIE — negative control, lane not called
q8 2.19 / 1.60 2.06 / 1.82 −6.0% / +13.5% overlapping, both TIE — no regression, stays a win

100k (run 1 / run 2): q1 42.96/42.59 → 31.66/31.45 (−26.3% / −26.2%, disjoint both);
q4 12.29/12.19 → 9.57/10.43 (−22.1% / −14.4%, disjoint both); q3 15.41/15.26 → 13.41/13.88
(−13.0% disjoint / −9.1% overlapping); q5 and q8 overlapping in both. 100k was already a win
column: q1 3.47× → 4.70×, q3 1.98× → 2.17×, q4 1.09× → 1.28×, q8 unchanged.

Explicit verdicts

  • q1 no-regression: PASS, and better than that. q1 is the disqualifying risk — the same function
    serves it — and it is faster, by 32% at 20k and 26% at 100k, non-overlapping in all four runs,
    with its win over Kuzu widening. There is no trade to surface.
  • q3 does NOT flip to a win. It goes from a 1.37× loss to a TIE: the PR slot range
    (5.20–5.95 ms) overlaps Kuzu's (5.46–6.98 ms). The nominal median ratio is 1.14× in GFQL's favour
    and I am not calling that a win. This is thinner than the ~1.10× flip that was projected; the
    projection is superseded by the measurement.
  • q4 does NOT flip. 1.92× loss on the board / 2.10× this session → 1.51×, still a loss.
    Narrows as projected.
  • q5/q8 untouched: tie arms at both scales in both runs; q8 remains a win.

Value identity: one canonical row set per query across every slot of every run — 0 mismatches.


Shape enumeration, and what is declined

26 cypher shapes were probed end to end, with engagement instrumented rather than assumed. The
fused lane serves 14, declines 5, and is never reached by 7 — those seven are declined by
the fast path's own guard first (undirected, reverse, two-hop, named edge, no aggregate, aggregate
without a group key, SKIP+LIMIT), and the tests assert the fast path was called and declined,
which is what makes the empty fused-call list meaningful. Three further shapes (min/max over an
alias property, count(DISTINCT ..), a repeated MATCH alias) are declined by the cypher lowering
and never reach the fast path at all; min/max are still translated and unit-tested, because the
fast path is reachable from a hand-built chain even though cypher cannot get there today.

Declines, each with its reason and each pinned by a test:

  1. a non-eager-polars input frame (LazyFrame schema probes warn and cost; a non-polars frame has no
    polars .lazy());
  2. a property column missing from its alias' node frame. The eager twin discovers this
    mid-chain and returns None from the whole fast path, so the query then raises the polars
    NotImplementedError. A fused plan built before that check would have silently answered instead —
    so the guard is hoisted ahead of plan construction, and the test pins the raise;
  3. source and destination bound to the same edge column;
  4. a projected column colliding with an endpoint column or with the internal lookup key;
  5. an untranslatable aggregate;
  6. a result row order that ORDER BY does not fully determine — see below.

The order-totality gate is a measurement, not a precaution

Without every group key in the sort, the eager twin's row order falls back to
maintain_order=True group first-appearance order over an eager join output — a property a
lazy plan is free to change by re-ordering or re-siding joins, and one that LIMIT turns from a
cosmetic difference into a different row set.

I built an ungated variant of the same lazy plan and compared it against the eager twin over 4
graph sizes × 4 seeds × 4 order-undetermined shapes: 64 comparisons, 47 divergent, and every
LIMIT-bearing divergence differed in row set, not merely row order. The positive side is pinned
too: with a total sort, 128/128 comparisons at up to 60k nodes / 137 groups are identical to both
the eager twin and pandas.


Verification

  • Differential: 14 served shapes × 10 graph fixtures × 2 polars engines = 280 parametrized
    cases
    , each comparing against both the eager code and the pandas oracle, row-order and
    column-order sensitively
    . Fixtures: base, duplicate node rows on the end arm, duplicate node
    rows on the start arm, self-loops + parallel edges, string ids, dangling endpoints, empty
    edges, no matching nodes, null properties, all-null group key. 0 divergences from the eager
    code.
    (A scratch harness over the wider 19-shape × 9-graph corpus gave the same answer: 342
    comparisons, 252 with the lane engaged, 0 divergences from the eager code — every divergence it
    reported was against pandas, and the eager code reproduced each one, which is how the pre-existing
    multiplicity asymmetry below was identified as pre-existing rather than introduced.)
  • Mutation testing: 35 mutants, 28 killed, 7 survivors — every survivor investigated.
    The first round killed 19/25 and the survivor analysis found two real gaps, both now closed:
    a composite mutant (start-arm semi-join → non-deduplicated inner join) was invisible because every
    duplicate-node fixture put its duplicates on the end arm, and a group-key-reordering mutant was
    invisible because dict equality ignores key order. Fixtures and the comparison were strengthened
    and both mutants now die. The 7 remaining survivors are provably equivalent rewrites, not gaps:
    semiinner on either arm (the right side is a .unique()'d single column, so an inner join
    emits each left row at most once and right_on coalesces the key — and the composite with the
    dedup removed is killed); dropping .unique() on a semi key side (duplicates on a semi key side
    can neither change nor multiply rows — and the composite with semiinner is killed);
    innerleft on the property join (the preceding semi-joins already guarantee every endpoint is
    present — and the composite that also drops that semi-join is killed);
    maintain_order=TrueFalse (provably redundant under the totality gate, since the sort is
    total; left in place so the plan stays character-identical to the twin);
    pl.len()pl.first().len() inside an agg (a synonym — the real count mutants, off-by-one and
    n_unique, both die); and a shared lookup-key name across the two aliases (each join consumes the
    key before the next lookup frame is built).
  • GPU receipt: dgx-spark GB10, --gpus all, graphistry/test-rapids-official:26.02-gfql-polars
    (cudf 26.02.01, cudf_polars present, polars 1.35.2 — a version-drift check against the local
    1.42): 373 passed, 0 skipped. Every polars-gpu and cudf parameter actually executed. Local
    CPU run: 197 passed, 176 skipped (those are exactly the GPU parameters).
  • Perf lock held for every timed experiment and released — LOCK-FREE-CONFIRMED, no runner
    processes left.
  • bin/lint.sh and bin/typecheck.sh clean. No Any, no cast at the call boundary, no getattr.

Structural lock-in

pyg-bench graphistry/pyg-bench#115 — structural, not a timing gate (the win is a constant-factor
plan fusion, so a growth-ratio gate would keep passing after the lane stopped engaging). On the
board's own query text it asserts q1/q3/q4 are served, q5–q9 never enter the lane, fused ==
forced-eager == pandas with row and column order, and that an order-undetermined variant of q4
declines. Verified to fail against a build without the lane.

Disclosed, pre-existing, deliberately not changed here

The polars property lookup is not deduplicated by node id while the pandas one is
(drop_duplicates(subset=[node_col])), so a node table carrying the same id twice multiplies matched
rows on polars only. That asymmetry predates this change; the fused lane reproduces the eager polars
answer exactly rather than quietly "fixing" it inside a performance change, and a test pins both
sides on both arms of the hop.

Rebase note (three PRs touch this file)

The diff is deliberately mechanically separate from the other two in-flight changes to
gfql_fast_paths.py. In master coordinates: #1800 edits _RESIDUAL_TOLOWER_EQ around line 617;
#1816 inserts at line 1546 and edits _execute_two_hop_count_fast_path at line 1904; this PR
inserts at line 1574 and edits one line at 1713
inside
_execute_single_hop_grouped_aggregate_fast_path. Every pair is far more than three lines apart, so
all three merge cleanly in any order. Tests go in a new file rather than into test_lowering.py
(#1816) or test_residual_polars_native.py (#1800); the only shared file is CHANGELOG.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB

…y plan

`_execute_single_hop_grouped_aggregate_fast_path` serves the cypher shape

    MATCH (a {..})-[{..}]->(b {..}) [WHERE ..]
    RETURN <alias>.<prop> AS k, <agg> AS v ORDER BY .. [LIMIT n]

which is three of the nine matched graph-benchmark cells (q1, q3, q4). Its
polars branch chained ~7 EAGER ops -- two semi-joins, two property joins, a
group_by, a sort and a head, each its own lazy().collect(_eager=True) -- so
every intermediate materialized, the select([src, dst]) projection could not
be pushed into the semi-joins, and the head() could not reach into the plan
at all. 74-96% of each of those queries' wall time sat inside those collects.

The same op sequence is now built as ONE lazy plan collected once. The algebra
is character-identical to the eager twin (same .unique() id frames, same
semi-joins, same UN-deduplicated property lookups, same
group_by(maintain_order=True).agg(..), same per-key nulls_last sort, same
head), so the value is identical, row ORDER included.

STRICTLY ADDITIVE: the eager code is untouched and is the fallback on every
decline, so the blast radius is a decline away from zero.

DECLINES (never a different answer, only a forgone speedup):
  * a non-eager-polars input frame;
  * a property column missing from its alias' node frame -- the eager twin
    discovers this MID-CHAIN and declines the WHOLE fast path, so the guard
    is HOISTED ahead of plan construction; otherwise the fused lane would
    answer a query that is supposed to raise;
  * source and destination bound to the same edge column;
  * a projected column colliding with an endpoint column or the lookup key;
  * an untranslatable aggregate;
  * a result row order that ORDER BY does not fully determine.

That last one is the correctness crux and it is MEASURED, not assumed:
without every group key in the sort, the twin's order falls back to
maintain_order=True group first-appearance order over an EAGER join output,
which a lazy plan is free to change. An ungated variant of the same plan
diverged from the twin in 47 of 64 comparisons (4 graph sizes x 4 seeds x 4
order-undetermined shapes), and under LIMIT the divergence is a different ROW
SET, not merely a different row order.

MEASURED, dgx-spark, matched q1-q9 lane, canonical query text (gb_queries.py,
md5 6e7ae268a5a41742587fcb87854b6e27 @ pyg-bench 7e5ce3b), one perf lock per
experiment, master tree vs PR tree position-balanced M P P M P M M P, per-slot
medians, replicated in a second independent run:

  20k    q1 13.31 -> 8.96 (-32.7%)   q3 8.54 -> 5.53 (-35.3%)
         q4  6.99 -> 5.05 (-27.8%)   all non-overlapping in both runs
  100k   q1 42.59 -> 31.45 (-26.2%)  q4 12.19 -> 10.43 (-14.4%)
         q3 -9.1%..-13.0%

vs SAME-SESSION embedded Kuzu at 20k: q1 widens 1.12x -> 1.66x; q3 moves from
a 1.37x LOSS to a TIE (slot ranges overlap -- a tie, NOT a win); q4 narrows
from 2.10x to 1.51x and REMAINS A LOSS. q5 and q8 are ties at both scales in
both runs and q8 stays a win -- this lane is not called for them.

Value identity: one canonical row set per query across every slot of every
run. Differential over 19 shapes x 10 graphs, compared row-order AND
column-order sensitively against both the eager code and the pandas oracle:
zero divergences from the eager code.

DISCLOSED, pre-existing, deliberately NOT changed here: the polars property
lookup is not deduplicated by node id while the pandas one is, so a node table
carrying the same id twice multiplies matched rows on polars only. The fused
lane reproduces the eager polars answer exactly and a test pins both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
@lmeyerov

Copy link
Copy Markdown
Contributor Author

⚠️ CI: test-polars (3.12) was CANCELLED at the job's 10-minute timeout, and that is this PR's doing

Settled CI on f20a5c0: 75 pass, 1 cancel, 1 skipping (changed-line-coverage, skipped by its own gate), 0 fail.
The one non-pass is not a test failure — it is the lane running out of clock:

test-polars (3.12)  job 04:00:56 -> 04:11:12  conclusion=cancelled   (timeout-minutes: 10)
  Install Python dependencies      04:01:04 -> 04:01:15   success
  Polars tests with coverage       04:01:15 -> 04:11:08   CANCELLED  (9m53s, never finished)
  Polars GFQL coverage audit       skipped

Every other step in that job succeeded, every other test-polars cell (3.9/3.10/3.11/3.13/3.14) passed,
and the same tests pass locally (197 passed) and on dgx-spark under --gpus all (373 passed, 0 skipped).
So this is a wall-clock budget problem, not a correctness one.

Why it is mine. That cell had essentially no margin before this PR — measured on CI in #1814:
the coverage cell takes 501s of a 600s budget on master. This PR adds ~200 tests to
bin/test-polars.sh's file list (deliberately — #1805 exists precisely so a polars-gated test cannot run
in no lane), and that consumed the remainder. I am not going to describe a lane I pushed over its
timeout as "a pre-existing flake".

The fix already exists and is not this PR. #1814 parallelizes the lane (-n auto --maxprocesses 4 --dist load), measured on CI at 501s → 322s / 273s. With it in, this PR's lane has minutes of margin
rather than seconds. I have pushed a validation branch — validate/h2-fused-on-1814 = #1814's head with
this PR's commit cherry-picked on top — and dispatched CI on it to turn "should fit" into a measurement
rather than an argument; I will post the resulting test-polars (3.12) duration here either way.

What I did NOT do: touch .github/workflows/ci.yml to raise the timeout, and I did not trim the test
file to squeeze under the wire — the shape enumeration and the differential corpus are the evidence this
change rests on, and shrinking them to fit a scheduling budget would be trading the wrong thing.

Suggested merge order: land #1814 first, then this.

@lmeyerov

Copy link
Copy Markdown
Contributor Author

✅ Validation result: with #1814 in, the lane fits — measured, not argued

validate/h2-fused-on-1814 = #1814's head (1e499f5) with this PR's commit cherry-picked on top
(883edea). CI dispatched on it: run 30328220396
the whole run concluded success, and every test-polars cell passed:

cell duration conclusion
test-polars (3.11) 2m23s success
test-polars (3.10) 2m32s success
test-polars (3.9) 2m54s success
test-polars (3.13) 3m13s success
test-polars (3.14) 3m30s success
test-polars (3.12) — the coverage cell that was cancelled here 6m02s success

The decisive comparison, same tests, same commit content, one variable:

this PR alone (f20a5c0)              "Polars tests with coverage"  593s  -> CANCELLED at the 10-min job cap
this PR on top of #1814 (883edea)    "Polars tests with coverage"  339s  -> success, +  coverage audit passed
                                     job total 6m02s of a 10m budget  =>  ~4 minutes of margin

So the cancel was a scheduling-budget problem with an already-open fix, exactly as described — 593s
incomplete becomes 339s complete
, and the "Polars GFQL coverage audit (per-file floors)" step, which
never got to run here, passes there too.

Merge order confirmed by measurement: land #1814, then this. No change to this PR is required, and
.github/workflows/ci.yml stays untouched.

The validation branch has served its purpose and is deleted.

@lmeyerov

Copy link
Copy Markdown
Contributor Author

Generality, stated independently of the benchmark — including the part that cuts against this PR

The body above justifies the lane as "three of the nine matched graph-benchmark cells — q1, q3 and q4." That is a benchmark justification, not a shape justification, and on its own it is not sufficient. Measured answer, on non-benchmark shapes:

Where the lane generalizes. dgx-spark, graphistry/test-rapids-official:26.02-gfql-polars, --gpus all, polars 1.35.2 in-image, 100k graph-benchmark. Engagement asserted, not assumed (served/called counted by monkeypatch):

non-benchmark variant served delta
v_q4_no_limit (no LIMIT) 11/11 −10% … −31%
v_two_group_keys (two grouping keys) 11/11 −10% … −31%
v_2hop_count_star 11/11 −10% … −31%

So the lane is not q1/q3/q4-shaped. It serves the grouped-aggregate shape, and 7 of the 10 grouped-aggregate variants probed.

Where it does NOT — and this is the honest limitation. The order-totality gate declines the form an analyst actually writes:

variant served wall eager share
… ORDER BY personCounts DESC, countries LIMIT 3 (q4 as the benchmark writes it) 11/11 11.65 ms 31%
… ORDER BY personCounts DESC LIMIT 3 (the top-N a human writes) 0/11 13.38 ms 89%
(no ORDER BY — "count by country") 0/11 13.49 ms 87%
q1 without the personID tiebreaker 0/11 45.75 ms 97%

q4's benchmark text happens to name every group key, so its sort is total and the lane engages. Drop the tiebreaker and it declines and stays 87–97% eager. That asymmetry is worth naming in the PR rather than leaving for a reviewer to find.

Sizing the prize behind that gate, gate removed in a scratch monkeypatch (same plan), 8 interleaved slots:

shape gated ungated delta verdict
v_q4_no_orderby 14.09 11.79 −16.3% disjoint
v_q4_orderby_agg_only 14.58 11.24 −22.9% TIE (overlap)
v_q1_orderby_agg_only 46.12 35.64 −22.7% TIE
v_q3_no_orderby 16.63 14.43 −13.2% TIE

13–23%, most of it inside the noise band at n=4 slots. Values were identical on this dataset, but that is not safety evidence — one dataset, one seed, and the earlier 47-of-64 row-order divergence sweep stands unrefuted.

Conclusion: the gate stays, and this PR is landable as-is

The order-totality gate is a measured correctness boundary, not a shape gate reverse-engineered from the benchmark — removing it changes which rows come back under a non-total ORDER BY … LIMIT. Keeping it is correct here.

Widening it is a cross-engine row-order policy decision, not an engineering one, and belongs in its own change: when LIMIT is present and ORDER BY is not total, append the group keys as an implicit deterministic tiebreaker. Cypher leaves that order unspecified, so it is spec-legal, and it would remove the gate for every shape at once — but pandas must match, and it needs a differential sweep before it can land.

Two corrections to the body above

  1. "the blast radius is a decline away from zero" — narrowness is simultaneously a safety property and a benchmark-hacking signature. It should not be cited as a virtue without the generality evidence above; with that evidence, the claim stands.
  2. Sizing the lane against "three of nine benchmark cells" is the wrong frame even when the resulting code is general. The frame that survives review is the one in this comment: which shapes, benchmark-independent, and where does it stop.

Unrelated pre-existing bug this work surfaced

These lanes bare-call LazyFrame.collect() rather than gfql.lazy.collect(), so engine='polars-gpu' silently runs on CPU — filed as #1824. It is on master (from #1755), not introduced here, and is not a blocker for this PR.

@lmeyerov
lmeyerov merged commit 7d0d0fc into master Jul 28, 2026
77 checks passed
@lmeyerov
lmeyerov deleted the perf/gfql-h2-grouped-agg-fused branch July 28, 2026 07:40
lmeyerov added a commit that referenced this pull request Jul 28, 2026
Only CHANGELOG conflicted, and both entries are kept -- #1823's fused single-hop
grouped aggregate and this PR's constant folding are disjoint lanes (#1823 serves
q1-q4, this serves q5-q7), and gfql_fast_paths.py auto-merged with no overlap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
lmeyerov added a commit that referenced this pull request Jul 28, 2026
… tests actually run

The entry claimed a "288-cell pandas x polars x cuDF sweep over 8 aggregates x
9 dtypes ... zero divergences (24 before)". Nothing in the repo produces 288:
the shipped matrix is `_MATRIX_AGGS` (6: count/sum/avg/min/max/collect) x
`_MATRIX_COLS` (10: 5 numeric + 4 non-numeric + 1 all-null) x 3 engines
(polars/cudf/polars-gpu), i.e. 180 cells, each compared against the pandas
oracle on BOTH the value and the error class. 288 = 8x9x4, which matches
neither the agg list, the column list, nor the engine list, and the prose
names three engines while 4 would be needed to reach it. The "(24 before)"
baseline has no artifact behind it either, so it is dropped rather than
restated -- an unverifiable number is worse than no number.

Also: "the aggregate kernels are written three times" was true when this
branch was cut and is not now. #1823 added a fused lazy lane in front of the
OLAP single-hop grouped fast path, making four -- which is exactly why this
branch had to add a decline to that lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
lmeyerov added a commit that referenced this pull request Jul 28, 2026
My previous merge of this branch was built by a helper that assumes a branch
ADDS a CHANGELOG entry and re-inserts it under its section. This branch does
not add one -- it REWRITES #1823's existing entry -- so the helper produced the
entry TWICE: master's uncorrected line plus the corrected one, which left the
withdrawn claim still present in the file the PR was meant to fix.

Rebuilt from master with the correction applied as an in-place replacement,
asserting both that the anchor occurs exactly once on master before the edit
and that the entry occurs exactly once after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
lmeyerov added a commit that referenced this pull request Jul 28, 2026
Docs sit at the TOP of the stack so a change-request here cannot block the code
PRs underneath it.

CHANGELOG hand-resolved: this branch REPLACES #1823's entry rather than adding
one, so neither a textual 3-way nor the add-only fallback applies. Built as the
parent file with the q8 withdrawal applied in place, asserting the anchor occurs
exactly once before the edit and the entry exactly once after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
lmeyerov added a commit that referenced this pull request Jul 28, 2026
Docs sits at the TOP so a change-request here cannot block the code underneath.

CHANGELOG hand-resolved: this branch REPLACES #1823's entry rather than adding
one, so neither a textual 3-way nor the add-only fallback applies. Built as the
parent file with the q8 withdrawal applied in place, asserting the anchor occurs
exactly once before the edit and the entry exactly once after.

Rebuilt from this branch's clean pre-stack head: an earlier stacking attempt
uploaded child blobs over already-merged ones, which propagated a dropped
POLARS_TEST_FILES entry up the whole stack and turned test-gfql-core red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
pull Bot pushed a commit to admariner/pygraphistry that referenced this pull request Jul 28, 2026
…istry#1825)

graphistry#1823's entry closed with "q5 and q8 are ties at both scales in both runs,
and q8 stays a win over Kuzu". The first half is a real measurement of what
that lane does. The second half is a board-position claim that does not
survive re-measurement, and it is now merged on master where it round-trips
as fact.

q8's headline number is a CROSS-CALL MEMOIZATION artifact. The two-hop
equal-domain degree counts are cached onto the CALLER's Plottable keyed by
id(), so the figure quoted on the board is a warm repeat call:

  warm        2.02 ms @20k   5.09 ms @100k
  cold (memo delattr'd)  13.18        49.68
  fresh Plottable        14.60        53.02
  bind_only control       0.02         0.02   <- rules out re-binding

A ONE-SHOT q8 therefore loses to embedded Kuzu by 3.1-5.2x at 20k and
2.8-6.0x at 100k. The memo itself is filed as graphistry#1825, because keying a cache
by id() on the caller's object also returns a stale answer after an in-place
edge or node mutation -- so the cell is not merely optimistic, the mechanism
behind it is a bug.

Reconciled rather than deleted: the withdrawn sentence is quoted in place so
the correction is traceable from the claim, and the measurement graphistry#1823 actually
reports -- that its lane leaves q8 unchanged -- is explicitly preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant