Skip to content

perf(gfql): pay #2039's column restore only when an alias can shadow a column - #2104

Merged
lmeyerov merged 3 commits into
masterfrom
perf/gfql-2039-restore-guard
Sep 20, 2026
Merged

lmeyerov merged 3 commits into
masterfrom
perf/gfql-2039-restore-guard

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Fixes #2102.

The regression

#2039 fixed a real bug — an edge alias sharing its name with a column its own step filters on was stamped as a boolean marker, and the backward pass then re-filtered that marker as the column and raised. The repair re-joins the graph's whole edge frame against each step's edge ids to recover the original columns:

return edges.join(g_step._edges.select(pl.col(edge_id)), on=edge_id, how="semi")

It runs unconditionally, in two places (backward pass and pruned re-execution). On LiveJournal that is a semi-join over 34,681,189 edge rows, twice per hop.

Bisected on dgx against the ladder's bulk hop — 3 reps per tree, identical result sizes, load-gated, one job at a time:

tree hop1 (ms)
6417cdd30 (parent) 351.1, 328.0, 348.9 CLEAN
a59990981 (#2039) 421.8, 438.9, 468.3 REGRESSED

Max clean 351.1 < min regressed 421.8 — disjoint.

The fix is a guard, not a revert

The shadowing needs an ASTEdge alias named like an existing edge column. When no alias can collide, the step's own edges already carry the graph's columns and the restore is pure cost. _edge_alias_can_shadow_column(ops, g) decides once per chain.

It fails toward correctness: unprovable inputs (no edge frame, unreadable columns) answer True and keep the restore.

Measured, same lane

before after
hop1 403.9, 423.2, 426.1 332.8, 269.1, 280.9
hop2 7347.5, 7366.4, 7347.5 6888.5, 6742.5, 6902.5

−33% on hop1, back inside the pre-#2039 clean band (259.5–351.8) and disjoint from the regressed band. hop2 recovers to at/below its clean band.

Correctness

Why it went unnoticed

No board in CI or the release suite exercises a bulk hop over tens of millions of edges. It surfaced only because the GraphFrames ladder — the one board that does — was being re-measured after being waived as stale. The waiver was hiding a live regression.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp

lmeyerov and others added 3 commits September 19, 2026 23:52
…a column

#2039 fixed a real bug: an edge alias sharing its name with a column its own step filters
on was stamped as a boolean marker, and the backward pass then re-filtered that marker as
the column and raised. The repair re-joins the graph's WHOLE edge frame against each step's
edge ids to recover the original columns.

That restore is unconditional, and it is expensive: on LiveJournal it semi-joins 34,681,189
edge rows, twice per hop. Bisected on dgx against the ladder's bulk hop, 3 reps per tree,
identical result sizes, load-gated, one job at a time:

  6417cdd (parent)  hop1 351.1, 328.0, 348.9   CLEAN
  a599909 (#2039)   hop1 421.8, 438.9, 468.3   REGRESSED   <- disjoint

The shadowing needs an ASTEdge alias NAMED like an existing edge column. When no alias can
collide, the step's own edges already carry the graph's columns and the restore is pure cost.
_edge_alias_can_shadow_column decides that once per chain; unprovable inputs (no edge frame,
unreadable columns) answer True and keep the restore, so the guard fails toward correctness.

Measured with the guard, same lane:

  hop1  403.9, 423.2, 426.1  ->  332.8, 269.1, 280.9   (-33%, back inside the pre-#2039
                                                        band 259.5-351.8, disjoint from
                                                        the regressed band)
  hop2  7347.5, 7366.4       ->  6888.5, 6742.5, 6902.5

Not a revert: #2039's collision pins, the alias-scoping suite and #2051's dedup pins all
pass, with a failure set identical to the unmodified tree (34, every one a pre-existing
cuDF env artifact). The guard is verified to ENGAGE and to still fire on real collisions
rather than being green over dead code.

q1-q9 at 100k are unchanged (they never regressed): q1 22.92->23.02, q8 12.90->13.28, q3
8.96->8.05, all within run-to-run spread with identical rows.

Found only because the GraphFrames ladder was being re-measured; no board in CI exercises a
bulk hop over tens of millions of edges, so this sat undetected through the 0.60 release.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
The comment-encoding guard requires a contract stated by NAME, with the issue carried by the
pin's test name. Drops two issue refs from the source; the behaviour pins in
test_chain_alias_shadow_restore_guard.py carry the provenance.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
Two CI guards caught the same omission: the module calls engine='polars' but ran in
test-core-python and test-gfql-core, where polars is not installed, and
test_polars_lane_completeness flagged that it belonged to no polars lane at all.

Adds the pytest.importorskip('polars') the sibling polars modules use, and registers the file
in POLARS_TEST_FILES so the pins actually execute somewhere rather than silently skipping.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov
lmeyerov merged commit 62df29a into master Sep 20, 2026
90 checks passed
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.

Bulk g.hop() regression: LJ 1-hop +27%, 2-hop +6%, disjoint ranges; entered during the 0.60 stack, not the perf PRs

1 participant