Skip to content

Fix Perf Bug and add Perf Testing to CI - #160

Merged
ColtonPayne merged 9 commits into
mainfrom
Bruno-Faster-Pyreason
Aug 24, 2026
Merged

Fix Perf Bug and add Perf Testing to CI#160
ColtonPayne merged 9 commits into
mainfrom
Bruno-Faster-Pyreason

Conversation

@BAF1927

@BAF1927 BAF1927 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

For this tests, the implementation consistently produced identical observable outputs and scaled substantially better as number of nodes inferred by the rule increased. Median speedup rose from 1.37× at 100 people to 6.10× at 2,000 people

Test Results:

Test Size (People) Original Optimized Speedup
100 0.232 s 0.169 s 1.37×
500 2.320 s 1.164 s 1.99×
1,000 9.282 s 3.660 s 2.54×
2,000 37.127 s 6.089 s 6.10×
Screenshot 2026-07-27 at 10 22 03 a m Screenshot 2026-07-27 at 10 22 22 a m

In `_ground_rule`, the node-head branch called
`check_all_clause_satisfaction` once per head grounding, inside
`for head_grounding in groundings[head_var_1]`. The check does not take
`head_grounding`, so the only way its result could vary between
iterations is if the loop body mutated state the check reads. The body
has exactly one such mutation, `_add_node`, guarded by
`add_head_var_node_to_graph` — and that flag is only ever set on the
branch that assigns `groundings[head_var_1]` a single-element list. When
the mutation can fire, the loop runs once, so there is nothing to differ.

The check is therefore loop-invariant, and the work it repeated scaled
with the number of head groundings: O(clauses x groundings) re-evaluated
K times per rule firing.

Run it once, before the loop, and enter the loop only if it passes. Guard
it behind a non-empty head-grounding check so a rule with no heads does
no work. `satisfaction` is not read after this block, so initializing it
to False changes nothing observable.

The edge-head branch keeps its per-iteration check: that one reads
`temp_groundings`, which is rebuilt for each head pair and is genuinely
not invariant.

Measured on a Friends/owns graph with many candidate heads, identical
observable output in both variants:

    people   before    after     speedup
       100   0.232 s   0.169 s     1.37x
       500   2.320 s   1.164 s     1.99x
     1,000   9.282 s   3.660 s     2.54x
     2,000  37.127 s   6.089 s     6.10x

Applied identically to interpretation.py, interpretation_fp.py and
interpretation_parallel.py; the latter still differs from the first only
in the parallel= decorator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ColtonPayne and others added 8 commits August 14, 2026 15:52
Ruff 0.16.0 (2026-07-23) expanded its default rule set from 59 rules to
413 — the first change to that default since v0.1.0. The workflow installs
ruff unpinned and pyproject.toml sets no `select`, so the repo silently
inherited all 413 and now reports ~289 findings (SIM, UP, RUF, I001, BLE,
EXE, PIE, FURB, C4) on code that was green in May.

This is not specific to any one branch: main has not run CI since
2026-05-13 and fails the same way, so every PR opened after 0.16.0 landed
was broken on arrival regardless of its contents.

Pin to 0.15.22, the last release carrying the old E4/E7/E9/F default, so
lint results depend on the code rather than on when the job happened to
run. Note that 0.16 also dropped 18 rules from the defaults, E741 among
them, so the existing per-file-ignore stays necessary under this pin.

Adopting the broader rule set remains an option, but it needs an explicit
`select` plus a deliberate one-time cleanup, reviewed on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No longer stores the pokec-2k fixtures, it now generates them in real time
Make the perf workflow's behavior deliberate instead of incidental, and cut
its dependency on an external host.

Gating
- Both jobs now carry symmetric conditions whose only filter is the
  workflow_dispatch size selector. pokec-10k already ran on every PR, but via
  an `if:` whose first clause short-circuits true for all non-dispatch events
  -- accidental, and left over from 25fe35a ("FOR TESTING"). pokec-2k was
  additionally excluded from the nightly cron for no stated reason.
- The two sizes run as parallel jobs, so the gate costs the longer of them
  (~11 min), while python-package-version-test.yml on the same PR takes
  29-35 min. Perf is never the critical path, and runner minutes are free on
  public repos, so 2k rides along at no wall-clock cost.
- 10k goes to --repeats 3, matching 2k, so its median is measured the same way
  the banked baseline was.
- Timeouts drop from 45/300 min to 20/30 min: real ceilings with ~3x headroom
  rather than placeholders that would let a hung job burn five hours.
- Add concurrency cancel-in-progress for pull_request only, so superseded PR
  runs stop while main and nightly history stays complete.

Fixtures
- Commit both fixtures gzipped (76 KB + 509 KB) and gunzip them in CI. This
  removes the actions/cache steps and the two curl calls to snap.stanford.edu:
  no cold-miss download, no third-party outage in the gate's path, and fork
  PRs -- which cannot write to the Actions cache -- behave like branch PRs.
- Regenerated from the SNAP dumps with the current generate_fixtures.py. The
  2k GraphML is byte-identical to the one previously committed at 25fe35a; the
  10k has never been in git before. Both match the documented reference counts
  (2k: 18,315 friend / 1,203 hasPet / 5 customers; 10k: 121,716 / 6,204 / 14).
- Compressing with the no-timestamp flag keeps the archives byte-reproducible
  from identical GraphML.
- .gitignore now excludes only the unpacked .graphml build artifact.

README: describe the gate as intended, document why both sizes run per PR,
why LFS is the wrong tool at this size, and how to regenerate.
Follow-up on review notes that survived the previous commit.

Editable install
- The perf jobs installed the package non-editable, but run_bench.py's
  _spawn_child prepends the repo root to the child's PYTHONPATH, so the child
  always imports the checkout. The site-packages copy was never imported --
  confirmed locally where site-packages held 3.5.1 while the checkout was
  3.6.0 and the child still resolved to the checkout. Switching to -e removes
  the discrepancy and states that the checkout is what gets measured, which is
  the correct thing for a perf gate to measure.

run_bench.py
- Reject --repeats below 1 in the parent. Previously it reached
  statistics.median on an empty list and raised StatisticsError, which named
  neither the flag nor the cause.
- A child that exits 0 having printed nothing now fails with that sentence
  rather than a bare IndexError from splitlines()[-1].

Both run_bench.py paths were unreachable in CI; they only bite someone driving
the script by hand.

Two earlier notes are moot: restore-keys, because the actions/cache steps and
the SNAP downloads are gone, and cancel-in-progress, which the previous commit
added. A paths: filter is deliberately not added -- see the README.
@ColtonPayne ColtonPayne changed the title Improved Pyreason Speed and tests included Fix Perf Bug and add Perf Testing to CI Aug 24, 2026
@ColtonPayne
ColtonPayne merged commit 584d9f3 into main Aug 24, 2026
5 checks passed
@ColtonPayne
ColtonPayne deleted the Bruno-Faster-Pyreason branch August 24, 2026 14:35
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.

3 participants