Skip to content

Commit a18f909

Browse files
lmeyerovclaude
andcommitted
ci(polars): parallelize the polars lane so the coverage cell stops racing its timeout
`test-polars (3.12)` is the only cell that runs the coverage-instrumented pass plus the per-file coverage audit, and it had already been cancelled twice at its 10-minute budget (615s, 611s) with zero test failures — pure budget exhaustion, the same commit passing or failing depending on the runner it drew. Measured on the master run that produced this base commit, the script step alone is 484s of the cell's 501s. The lever that needs no workflow edit: pytest-xdist is already in the [test] extra, and test-gfql-core already runs `-n auto` under `--cov`, so coverage+xdist is established here. The main pytest phase now runs `-n auto --maxprocesses 4 --dist load`. Measured (py3.12, polars 1.43.1, pandas 3.0.5, POLARS_COV=1), pinned to 4 CPUs to stand in for a GitHub-hosted ubuntu-latest runner: 325.65s serial -> 109.34s parallel, 2.98x. Verified, not assumed: - pass/skip NODE-ID SETS (not counts) identical serial vs parallel: 2417 node ids, 2404 passed / 13 skipped, across 4-worker, 2-worker, --dist loadfile and 4-CPU-pinned runs - merged coverage is a strict superset of serial (28,480 vs 28,478 covered lines; zero lines lost, zero files dropped; the 2 extra are versioneer fallback lines in _version.py) - the second `--cov-append` invocation still appends into the xdist-produced data file (+1,791 lines, nothing lost) and the result is line-identical to the whole-script run - `bin/coverage_audit.py --profile gfql-polars` emits a byte-identical report (modulo its timestamp) from the parallel data, so `changed-line-coverage` sees no change `--dist load` rather than `loadfile` because one module (test_engine_polars_chain.py) is 69% of the lane's test time, which caps file-level distribution at ~1.4x; no test in this lane depends on execution order or cross-test module state. POLARS_XDIST=0 restores the serial path and POLARS_XDIST_DIST selects another distribution mode if that ever changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
1 parent 233b64c commit a18f909

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3636
- **Indexed bypass could return every node for `rows()` over an unnamed pattern**: the boundary accepted a `rows()` call carrying no binding ops over a middle with no aliases. That call reads the traversal-narrowed node table, but the bypass hands it the full graph, so the query would have returned all nodes. The gate now requires that the rows call actually consumes the whole middle as binding ops — either it already carries them, or the named-middle rewrite installs exactly them.
3737
- **Seeded property-RETURN dtype divergence on cuDF**: the lean projection applied the pandas rows-pivot artifact (int → float64, bool → object) on every engine, but cuDF's canonical pivot preserves the source dtypes — so the fast path returned `float64`/`object` where cuDF's own canonical path returns `int64`/`bool`. The cast rule is now engine-aware; the dtype-class decline guard is unchanged.
3838

39+
### Infrastructure
40+
- **The polars CI lane runs in parallel, so the coverage cell stops racing its timeout**: `test-polars (3.12)` is the only cell that runs the coverage-instrumented pass *and* the per-file coverage audit, and it had already been cancelled twice at its 10-minute budget (615s, 611s) with zero test failures — pure budget exhaustion, with the same commit passing or failing depending on the runner it drew. `bin/test-polars.sh` now runs its main phase under `pytest-xdist` (`-n auto --maxprocesses 4 --dist load`); `pytest-xdist` was already in the `[test]` extra and `test-gfql-core` already runs `-n auto` under `--cov`, so no dependency changes. `auto` resolves to the runner's core count (4 on a GitHub-hosted `ubuntu-latest`) and scales *down* on a 2-vCPU runner where a fixed `-n 4` could be slower than serial; `--maxprocesses` keeps a many-core dev box from fanning out one polars process per core. Verified rather than assumed, on py3.12 / polars 1.43.1 / pandas 3.0.5 with `POLARS_COV=1`: the pass/skip **node-id sets** (not just counts) are identical serial vs parallel across 4-worker, 2-worker and `--dist loadfile` runs (2417 node ids, 2404 passed / 13 skipped every time); the merged coverage data is a strict superset of the serial data (28,480 vs 28,478 covered lines, zero lines lost, zero files dropped); the second `--cov-append` invocation still appends into the xdist-produced data file (+1,791 lines, nothing lost); and `bin/coverage_audit.py --profile gfql-polars` emits a byte-identical report (modulo its timestamp) from the parallel data, so the downstream `changed-line-coverage` check sees no change. Measured lane wall-clock with coverage on: **354s serial → 112s parallel (3.2×)**. `--dist load` rather than `loadfile` because one module (`test_engine_polars_chain.py`) is 69% of the lane's test time, capping file-level distribution at ~1.4×; `POLARS_XDIST=0` restores the serial path and `POLARS_XDIST_DIST` selects another distribution mode should a future order-dependent test need one.
41+
3942
### Documentation
4043
- **GFQL pay-as-you-go resident indexing user guide**: New :doc:`Pay-As-You-Go Resident Indexing <gfql/indexing>` page — the lifecycle guide to resident indexes (`gfql_index_all()` / `gfql_index_edges()` / `create_index()` / `show_indexes()` / `drop_index()`): what the node-id + CSR in/out adjacency sidecars are, what engages them on 0.58.0 (seeded typed-hop fast paths incl. property RETURNs and property-seeded lookups per #1768/#1770, direct `g.hop()`; the general polars chain traversal honestly noted as not yet covered), the staleness/validity contract (identity + fingerprint; rebind invalidates; declines are safe — identical results either way), engine notes (polars needs `gfql_index_all(engine='polars')` until #1767), 0.58.0-tag measured numbers, and a runnable end-to-end example. Wired into the GFQL toctree + recommended paths alongside :doc:`Seeded Traversal Indexes <gfql/index_adjacency>`.
4144
- **GFQL performance docs: 0.58.0 release-tag-verified numbers, siloed in one page**: `gfql/performance.rst` is now the canonical benchmark-numbers page (alongside `gfql/index_adjacency.rst` for the index benchmarks) — a benchmark rerun updates it alone. It carries the 0.58.0 tag sweep (DGX Spark GB10, warm medians N=30; four-engine numbers cross-engine parity-verified, competitor pairs validated against expected result rows): seeded typed-hop fast path across all four engines (e.g. pandas 29.9→2.46ms, 12.1×), native chain form, resident-index covered-shape lookups (with the `gfql_index_all(engine='polars')` caveat / PR #1767), flat seeded-hop scaling on pandas (0.159–0.164ms from 0.25M to 32M edges), the one-keyword `engine='polars'` LDBC SNB SF1 seed-lookup win (1,299.6→106.1ms, 12.3×), LDBC SNB interactive SF1 vs Neo4j 5.26 same-box pairs (GFQL 4 of 5; Neo4j wins recent-replies — reported as-is), OLAP multi-join vs embedded Kuzu (q8 200×, q9 14.2×) with the honest inverse (Kuzu wins single-table aggregates 2–4×, seeded property-projection lookups 2.4–64×), plus the prior Orkut/LiveJournal bulk sweep (moved from `engines.rst`, dated once) and its methodology. All other pages — `engines.rst`, `quick.rst`, `about.rst`, `overview.rst`, `index.rst` — now carry stable qualitative claims (e.g. "often an order of magnitude faster on query-heavy workloads") that link into `performance.rst` instead of inline figures, replacing the stale "up to ~38×" headlines and avoiding scattered per-claim version labels.

bin/test-polars.sh

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ set -ex
55
# - Extra args are passed through to the pytest phase
66
# - Set POLARS_COV=1 to collect coverage over graphistry/compute; the coverage
77
# data file location is taken from $COVERAGE_FILE (as the CI py3.12 lane sets it)
8+
# - Set POLARS_XDIST to pick the pytest-xdist worker spec (default `auto`); `0` forces
9+
# the serial path, which is what an A/B of this lane compares against
810
# - Non-zero exit code on fail
911

1012
# Assume [polars,test] installed
@@ -37,15 +39,40 @@ POLARS_TEST_FILES=(
3739
graphistry/tests/test_engine_frame_helpers.py
3840
)
3941

42+
# PARALLELISM. The py3.12 cell of this lane is the coverage cell and has repeatedly run out
43+
# of its CI budget; xdist is the lever that does not require a workflow edit (pytest-xdist is
44+
# already in the [test] extra, and test-gfql-core already runs `-n auto` under --cov, so
45+
# coverage+xdist is an established combination in this repo).
46+
# * worker spec `auto` = os.cpu_count(): 4 on a GitHub-hosted ubuntu-latest runner, and it
47+
# scales DOWN on a 2-vCPU runner where a fixed `-n 4` could be slower than serial.
48+
# * --maxprocesses caps the count so a 24-core dev box does not fan out 24 polars processes
49+
# that then oversubscribe polars' own thread pool.
50+
# * --dist load (xdist's default) balances per test. `loadfile` was measured too: it is
51+
# bounded by the single largest module and only reaches 1.4x where `load` reaches 3.2x.
52+
# No test in this lane depends on execution order or on cross-test module state, and the
53+
# pass/skip node-id sets were compared serial-vs-parallel and are identical; POLARS_XDIST_DIST
54+
# is the escape hatch if a future order-dependent test needs `loadfile`/`loadscope`.
55+
XDIST_ARGS=()
56+
if [ "${POLARS_XDIST:-auto}" != "0" ]; then
57+
XDIST_ARGS=(
58+
-n "${POLARS_XDIST:-auto}"
59+
--maxprocesses "${POLARS_XDIST_MAX:-4}"
60+
--dist "${POLARS_XDIST_DIST:-load}"
61+
)
62+
fi
63+
4064
COV_ARGS=()
4165
if [ -n "${POLARS_COV:-}" ]; then
4266
COV_ARGS=(--cov=graphistry --cov-report=)
4367
fi
4468

45-
python -B -m pytest -vv "${COV_ARGS[@]}" "${POLARS_TEST_FILES[@]}" "$@"
69+
python -B -m pytest -vv "${XDIST_ARGS[@]}" "${COV_ARGS[@]}" "${POLARS_TEST_FILES[@]}" "$@"
4670

4771
# cypher-lowering polars-parametrized cases (round ties, lower/upper, =~, numeric fns);
48-
# appended into the same coverage data file when POLARS_COV=1 (CI audit reads it)
72+
# appended into the same coverage data file when POLARS_COV=1 (CI audit reads it).
73+
# Left SERIAL on purpose: it is one module and ~8s of the lane, so worker startup would eat
74+
# the gain. Appending into the data file the xdist phase produced is verified — the merged
75+
# result is line-for-line identical to running the whole script in one go.
4976
COV_APPEND_ARGS=()
5077
if [ -n "${POLARS_COV:-}" ]; then
5178
COV_APPEND_ARGS=(--cov=graphistry --cov-report= --cov-append)

0 commit comments

Comments
 (0)