Skip to content

perf(sinks): a loaded solver takes a linear constraint's new coefficients in place rather than loading the model again - #2072

Open
FBumann wants to merge 4 commits into
perf/update-coefficientsfrom
perf/push-coefficients
Open

FBumann wants to merge 4 commits into
perf/update-coefficientsfrom
perf/push-coefficients

Conversation

@FBumann

@FBumann FBumann commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: Do the next pr

Note

The following content was generated by AI.

A held HiGHS, Gurobi or Xpress solver now takes a linear row's changed coefficients in place and re-solves warm, instead of loading the whole model again. On top of #2071, update(...).solve() with a new coefficient takes 0.12–0.48× the time.

What changed
  • Solver.keeps asks for the same options and digest, and for every coefficient of a quadratic constraint's linear part as loaded: Gurobi loads a quadratic row whole, as a QConstr. It no longer asks the same of a linear row.
  • Solver.push is now the family's: it finds the matrix entries that moved by more than 1e-12 relative (the tolerance keeps used), reads each entry's row off row_starts, hands them to _change_coefficients, and then calls the member's _push for bounds, costs and right-hand sides. The held copy of the coefficients takes the pushed values, so it mirrors what the solver holds.
  • _change_coefficients: HiGHS changeCoeff, Gurobi chgCoeff (each one call per entry, the only form either library has), Xpress chgMCoef (one call).
  • The sinks README table, Handoff.structure's docstring, docs/howto/faster.md and docs/development/architecture.md (the labels paragraph and the "Structure digest" entry) say a linear row's coefficients are taken in place.
  • Bench: the ladder's coefficient window now expects a push rather than a load (RELOADS), and bench/README.md says so. That window exists for the case where an efficiency multiplies a variable, and this PR changes what that case costs. The first push of this PR left it expecting a load, and CI's Memory (heap) job failed on it; test_a_window_takes_the_path_its_change_names failed the same way locally (8 cases) before the fix.

An entry that appears or disappears, such as a coefficient falling to zero, moves the digest, so that model still loads again.

Probes, and coverage that moved

tests/test_update.py:

  • test_a_moved_coefficient_is_taken_in_place_and_taken_back, on every solver: eff * p <= cap per generator, after another constraint. eff doubles, then goes back. Each answer equals a fresh build's on the same solver, and the solver loads once.
  • test_a_quadratic_constraint_whose_linear_part_moved_loads_again (Gurobi): eff * p + p * p in one row. The objective matches a fresh build and the solver loads twice.
  • The rung a coefficient moved now keeps the solver. Its value went from 0.5 to 0.8, because at 0.5 plants a and b tie in the north: the warm solve stopped at the other optimum, with the same objective but another primal, and the rung compares primals. At 0.8 a alone is cheapest.
  • test_new_numbers_are_read_again_on_the_old_labels_only_where_nothing_else_can_move: a coefficient and a divisor now load once; a coefficient falling to zero still loads twice.

tests/test_order.py::test_a_coefficient_moved_by_one_part_in_a_billion_loads_the_solver_again is now …_reaches_the_loaded_solver: it reads HiGHS's held matrix and finds the moved values, with one load.

bench/test_harness.py::test_a_window_takes_the_path_its_change_names holds the coefficient window to a push on every case's 2xs and xs rungs.

Mutation table

Each guard broken by one text substitution, the full suite run (pytest -q -n 4, all three solvers installed), the file restored with git checkout --, __pycache__ dropped on both sides, the tree clean after each. At ec085fe; the bench change after it touches no guard.

mutation result
a quadratic row's linear part not compared in keeps caught, 1 failed
moved coefficients never pushed caught, 38 failed
the held copy not updated after a push caught, 6 failed
an entry's row read off the wrong side of its start caught, 29 failed
HiGHS changes no coefficient caught, 32 failed
Gurobi changes no coefficient caught, 3 failed
Xpress changes no coefficient caught, 3 failed
A/B of update and solve, HiGHS, this container (4 cores, not the publishing machine)

Base #2071's head 83a8ab7, head ec085fe. One fresh process per measurement, base and head alternated ABBA for three rounds. Each process builds and solves once, then times 6 calls of model.update({param: …}).solve(), each with the values ×0.9–1.1 at the same coordinates, and reports the median; the table gives the median over the three processes. Started once the load average fell below 1.5: 1.43 before, 1.00 after.

model, what changes base ms head ms head ÷ base head faster in loads, base / head
storage xs (10k columns), eta 116.3 55.8 0.48 3/3 7 / 1
storage s (100k columns), eta 1475.4 452.3 0.31 3/3 7 / 1
network s (100k columns), eta 2270.8 282.4 0.12 3/3 7 / 1
network m (1M columns), eta 65118.9 8744.7 0.13 3/3 7 / 1
storage xs, load, control: no coefficient moves 40.7 43.4 1.06 1/3 1 / 1

Per-process medians, ms: storage xs base 116.3 / 116.1 / 117.9, head 55.8 / 56.8 / 53.6; storage s base 1475.4 / 1472.5 / 1484.4, head 452.3 / 463.7 / 452.2; network s base 2270.8 / 2298.5 / 2245.0, head 292.9 / 282.4 / 278.4; network m base 64880.0 / 65118.9 / 65267.5, head 8588.6 / 8809.8 / 8744.7; control base 38.3 / 42.5 / 40.7, head 37.1 / 43.4 / 45.8. The control's two ranges overlap, which reads as noise: both sides compare the coefficients once per solve.

The gain is the load skipped and the warm start kept; neither is separated here.

Gates at 75bffa8, on mathspec 0.4.0

No pixi in this container, so each gate ran in a uv environment synced from #2065's uv.lock (--group dev --group docs --all-extras, Python 3.12, polars 2.0.0; highspy, gurobipy 13.0.3 and xpress 9.9.1 installed):

  • ruff check ., ruff format --check . (0.16.9): clean.
  • pyrefly check on the package (1.3.1): 0 errors, at ec085fe.
  • pytest -q -n 4: 5952 passed, 459 skipped, 4 xfailed.
  • pytest bench/test_harness.py (test-bench): 482 passed, 15 skipped.
  • pytest tests/test_docs_math.py tests/test_docstring_links.py (docs-test): 116 passed.

Not run: docs-build, the floors environment, test-via-pyarrow, and bench/test_ladder.py itself, whose benchmark fixture this environment does not have.

Not done. HiGHS and Gurobi take one call per moved entry; neither library has a call that takes several. The A/B is HiGHS only. The one window's README line went stale with #2069 and #2070; that fix is on #2070 and merged up. Stacked on #2071.

🤖 Generated with Claude Code

https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G

…ents in place rather than loading the model again

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0edd001f-de1c-400c-827b-fbdfdc1003f1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@read-the-docs-community

read-the-docs-community Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

@codspeed

codspeed Bot commented Oct 28, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 97.63%

1 warning

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
✅ 100 untouched benchmarks
⏩ 307 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_read[commitment-s-specsolve-parquet] 684.3 KB 220.7 KB ×3.1
⚡ test_window[storage-s-specsolve-highs-coefficient] 12.8 MB 6.8 MB +88.88%
⚡ test_window[network-s-specsolve-highs-coefficient] 15.2 MB 8.1 MB +87.65%
⚡ test_window[commitment-s-specsolve-highs-coefficient] 1.6 MB 1.1 MB +38.78%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/push-coefficients (75bffa8) with perf/update-coefficients (6a9c249)

Open in CodSpeed

Footnotes

  1. 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. ↩

This branch has not been deployed

No deployments
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.

2 participants