Repository navigation
perf(engine): an update that gives a bound, a right-hand side or a cost new numbers at the coordinates it had skips most of the build - #2070
Conversation
…st new numbers at the coordinates it had skips most of the build Where every name an update replaces is a parameter that no build reads for which columns, rows or matrix entries exist, attached at the coordinates it had, the engine keeps the labels and the matrix and reads again only the bounds, constant sides and objective those parameters feed. Anything else builds in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G
…ilds in full Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G
Documentation build overview
110 files changed ·
|
Merging this PR will improve performance by ×2.51 warning
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
…ch block where a fresh build lists it 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
Note
The following content was generated by AI.
An update whose parameters feed only bounds, constant sides and the objective, with the coordinates they had, keeps the labels and matrix and re-reads only what they feed. On top of #2069, such an update takes 0.17–0.33× the time; any other update builds in full, unchanged.
What changed
Engine.buildtakes the names an update replaced (Model.updatealready passes them, #2069). Whereassembly.numbers_onlyholds,assembly.refilledreplaces the build:numbers_only: every changed name is a parameter; none is structural; each is attached at exactly the coordinates the last build attached it at (equal height and no new coordinate, the door refusing duplicates); and no constraint it feeds dropped a row with no terms._structural_parameters) is a parameter a build reads for which columns, rows or matrix entries exist: a variable's or a constraint'swhere, acases:region's condition, a shift's or a window's named amount, and, inside a constraint, a factor of a variable-carrying product or a divisor of a variable-carrying numerator. A power of a variable is refused at load, so it has no branch. In the objective every use is read again, so none there is structural.refuse_null_constantsand the gap check ofconstant_sideread the rows a constraint labelled; the stored frame holds only the rows that kept a term. A constraint that dropped one builds in full, so a new divisor of zero on that row is refused as a fresh build refuses it.refilled: on the stored label frames, reads again each variable's bounds that name a changed parameter (_columns), each constraint's constant side that does (_compiled+_constant_side), and the whole objective if it reads one, and splices each share over the run of labels its declaration owns. Labels, matrix, quadratic rows and sets are the last build's.Measuredis the last build's with those entries replaced, and its bound and right-hand-side ranges put back in build order (_in_build_order), sodiagnostics()lists each block where a fresh build lists it._build_variableand_build_constraintcall the same_columns,_compiledand_constant_side, so the two paths share one copy of each.The held solver is untouched: it still compares the digest and the coefficients and takes the new numbers by value, or loads again where a quadratic constraint's right-hand side moved.
Docs:
docs/howto/faster.md("Keep the model loaded") and the labels paragraph ofdocs/development/architecture.mdsay what such an update skips.Coverage moved.
test_a_rebuild_leaves_the_held_solver_pinning_none_of_the_old_modelheld a weak reference to the old matrix, which the new model now shares by design. It holds one to the oldrowsinstead, which a newloadreplaces.Probes
tests/test_update.py:test_a_change_of_numbers_alone_hands_over_what_a_fresh_build_does, every port buttsp_mtz: each real-valued parameter in turn ×1.25 + 0.5, the model's hand-off anddiagnostics()compared with a fresh build of that data (frames to 1e-12,objby column; shape, omissions, sparse parameters and every range). Over 264 such updates in 42 ports, 177 take the numbers-only path, and every port has at least one. The diagnostics comparison came after a first version put a re-read block's range at the end of its frame: 29 of the 42 ports failed it before the fix.test_new_numbers_are_read_again_on_the_old_labels_only_where_nothing_else_can_move, one parameter per role: a bound, a constant side and a cost take the path and stay loaded; a variablewhere, a constraintwhere, a coefficient, a divisor, fewer coordinates and a new index build in full. Each also compares the hand-off and the optimum with a fresh build.test_a_parameter_that_picks_which_entries_exist_is_built_again: acases:condition over variables, and a shift's named offset.test_as_many_coordinates_but_other_ones_build_in_full: amissing: absentbound moved from one generator to another.test_new_numbers_on_a_row_the_build_dropped_are_refused_as_a_fresh_build_refuses_them: a sparse coefficient leaves a row with no term, and a divisor of zero there is refused in a fresh build's words.Outside the suite, on the bench models at
sandxs: storageloadandp_maxand networkloadandp_maxtake the path, sectordemandand nodalinstalleddo not, and all six hand-offs equal a fresh build's.Mutation table
Each guard broken by one text substitution, the full suite run (
pytest -q -n 4), the file restored withgit checkout --,__pycache__dropped on both sides, the tree clean after each. At07241eb; the "coordinates" row again atc4d2848with its probe; the last two rows atc70193d.wherenot readcases:condition not read07241eb, then caught bytest_as_many_coordinates_but_other_ones_build_in_full, added for itA/B of the update, this container (4 cores, not the publishing machine)
Base #2069's head
fdeee15, headc4d2848.c70193dsince only reorders two small dicts of ranges after the refill. One fresh process per measurement, base and head alternated ABBA for three rounds. Each process builds once, then times 12 calls ofmodel.update({param: …}), 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. Only the update is timed: no solve, no hand-off. Load average 0.95 before, 1.50 after.loadloadloadloadp_maxinstalled, control: awherereads itPer-process medians, ms: storage 2xs base 36.9 / 34.9 / 38.5, head 13.1 / 11.5 / 12.1; storage xs base 37.7 / 39.8 / 38.5, head 10.9 / 11.3 / 10.5; network s base 107.6 / 102.2 / 109.6, head 31.2 / 32.3 / 29.7; network m
loadbase 386.0 / 389.1 / 392.4, head 61.1 / 66.8 / 71.7; network mp_maxbase 407.4 / 384.5 / 380.2, head 120.2 / 127.4 / 106.1; nodal m base 68.4 / 62.3 / 71.9, head 68.1 / 65.5 / 68.8.Gates at c70193d, 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):ruff check .,ruff format --check .(0.16.9): clean.pyrefly checkon the package (1.3.1): 0 errors.pytest -q -n 4: 5942 passed, 459 skipped, 4 xfailed.pytest tests/test_docs_math.py tests/test_docstring_links.py(docs-test): 116 passed atc4d2848; no docstring link changed since.Not run:
docs-build, the floors environment,test-via-pyarrow.Not done. The door still re-reads dimensions, relations and assumptions on every update (#2069), about 10 ms of what is left on the storage model.
_structural_parameterswalks the program on every update; it is not cached, because aProgramis not hashable and no measured update spends time there. #2022 restructures the same build methods, so whichever of the two merges second needs a merge of the other. Stacked on #2069.🤖 Generated with Claude Code
https://claude.ai/code/session_018B3nA4oeSor7jjZ4vwux1G