Repository navigation
perf(engine): a model with many constraints builds about a third faster again - #2026
Conversation
…el, and every block's ranges are read together A constraint's block keeps its terms as they come and only reads which of its rows have any. Assembly.run sums repeated cells, drops zeros and orders the stacked matrix by (row, col) once, and reads every block's coefficient, right-hand-side and bound range in one grouped query per table rather than one query per block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
…jective's included Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
…the matrix is summed and ordered once Reading every block's range in one numpy pass cost dispatch and network at a million variables a fifth to a third more build time than the per-block polars probe, which runs on all threads. The matrix's single collapse stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
…an hashing them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
|
Note The following content was generated by AI. Folded into #2022. These commits are now on #2022's branch, Generated by Claude Code |
Merging this PR will degrade performance by 22.15%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_window[fleet-s-specsolve-highs-values] |
10.4 MB | 14.9 MB | -30.29% |
| ❌ | test_window[fleet-s-specsolve-highs-one] |
10.4 MB | 14.9 MB | -30.29% |
| ❌ | test_window[commitment-s-specsolve-highs-values] |
1.2 MB | 1.6 MB | -29.09% |
| ❌ | test_window[commitment-s-specsolve-highs-one] |
1.2 MB | 1.6 MB | -29.09% |
| ❌ | test_window[storage-s-specsolve-highs-one] |
11.7 MB | 15.8 MB | -25.96% |
| ❌ | test_window[storage-s-specsolve-highs-values] |
11.7 MB | 15.8 MB | -25.96% |
| ❌ | test_window[network-s-specsolve-highs-one] |
14.8 MB | 18.5 MB | -19.69% |
| ❌ | test_window[network-s-specsolve-highs-values] |
14.8 MB | 18.5 MB | -19.69% |
| ❌ | test_window[milp-s-specsolve-highs-one] |
21.9 MB | 27.2 MB | -19.67% |
| ❌ | test_window[milp-s-specsolve-highs-values] |
21.9 MB | 27.2 MB | -19.67% |
| ❌ | test_window[transport-s-specsolve-highs-one] |
13.1 MB | 13.9 MB | -5.69% |
| ❌ | test_window[transport-s-specsolve-highs-values] |
13.1 MB | 13.9 MB | -5.67% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/work-once-at-freeze (611d936) with claude/batched-build (a9fdd89)2
Footnotes
-
307 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
No successful run was found on
claude/batched-build(611d936) during the generation of this report, so cbfca83 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
Note
The following content was generated by AI.
The prompt answered a proposal: "Collapse and sort the matrix once, when the model is frozen, instead of per block."
Stacked on #2022. A constraint's terms are summed, pruned and ordered once for the whole model rather than once per constraint. The fixed cost per constraint falls from about 6 to 3.4–4 ms; models of a million variables build in the same time.
What changed, method, measurements, mutation table, what was tried and dropped
Why. A profile of a model with 128 variables and 128 constraints, each over 100 coordinates, put 22% of its build in each constraint's own
_collapsedand_sorted. Each constraint ran its order and repeat probes and its sort as separate small polars queries.What.
_blockkeeps a constraint's linear terms as they arrive. It reads which of its rows have any term with_rows_with_terms, which marks them in one pass over the block's own rows from 0. It drops its rows with no terms there.Assembly.runstacks every block's terms on the model's rows and sums repeated cells, drops zeros and orders by(row, col)in one_collapsedcall, under the build's own engine choice._per_block_ranges), so the range is the one the per-block collapse gave._magnitude_range._matrix_shareand_ordered_rowsare gone.Tried and dropped. Reading every block's range in one pass, with numpy or a grouped polars query, cut the cost per constraint to about 2.6 ms. But it made
dispatch/mandnetwork/m1.2–1.3× slower: one pass over 4M values on one thread against the per-block polars probe on all threads. A first version of this PR also found the rows with terms byunique(), which hashed a million unsorted rows and cost the 1M rungs about 10%.Method.
Engine.buildfrom tidied sources, the median of 5–15 builds per process, alternating process runs per arm, started below a 1-minute load of 1. 4-vCPU cloud container. Base: #2022's head,a9fdd89.At this branch's head, five runs per arm:
At the commit before
_rows_with_terms, which only adds speed: N variables and N constraints over 100 coordinates, two runs per arm.The slope from N = 8 to 128 falls from 6.15 to 4.05 ms per pair, and from 5.61 to 3.37 masked. At that commit the port models were 0.77–0.97×: genx 0.77, pypsa_ac_dc 0.81, pypsa_unit_commitment 0.83.
Mutation table, taken by hand:
tools.mutatecannot express an edited expression. Clean tree, restored withgit checkout --,__pycache__dropped before and after each run.stacked = terms)_block(→pass)seen[:] = True)Gates.
pixi run lint,pyrefly checkonsrc/,ruff format --check .,pixi run test: 5593 passed.test-benchnot run:bench/is untouched.Not done. The bound, right-hand-side and objective ranges are still one polars query per block, which is about 1.2 ms per pair at N = 128. Reading them only when diagnostics ask would remove that at every size. But
Measuredkeeps them pastclose(), so that is a design question, not part of this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_015YXphRT4nkmgh8LgdYMgCj
Generated by Claude Code