perf(sparse): trim the stages around the CSR solver hand-off - #992
FabianHofmann wants to merge 6 commits into
Conversation
…es via getModel MOSEK bound keys and names are built vectorised. The polars LP writer skips identity scaling, avoids re-sorting frozen rows and builds fewer string branches; output is byte-identical. HiGHS names, when requested, are attached through getModel/passModel so the Hessian survives.
…ntraction chunks Merge concatenates all CSR operands once instead of folding pairwise. Expression.solution maps labels directly and evaluates CSR rows without densifying or rebuilding the constraint matrix. Contraction chunks are sized by nonzeros instead of a fixed row count.
… read-back, one freeze gather Constraint blocks are gathered into preallocated CSR buffers without vstack or eliminate_zeros; scaling is skipped when identity and label lookups use the label dtype. Frozen constraints receive duals on active rows directly. Mask and non-empty filtering share one gather and sense conversion is vectorised.
Merging this PR will improve performance by 44.76%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | test_to_lp[nodal_balance_sparse-severity=0] |
3.9 MB | 2.5 MB | +54.8% |
| ⚡ | test_to_lp[nodal_balance_sparse-severity=50] |
3.9 MB | 2.5 MB | +54.71% |
| ⚡ | test_to_lp[nodal_balance_sparse-severity=100] |
3.9 MB | 2.5 MB | +54.71% |
| ⚡ | test_to_lp[knapsack-n=10000] |
2.1 MB | 1.4 MB | +52.34% |
| ⚡ | test_to_lp[storage-n=250] |
38.5 MB | 25.8 MB | +49.12% |
| ⚡ | test_to_lp[merge_balance-severity=0] |
2.7 MB | 1.8 MB | +48.43% |
| ⚡ | test_to_lp[rolling-severity=0] |
2 MB | 1.4 MB | +48.07% |
| ⚡ | test_to_lp[nodal_balance-severity=50] |
3.8 MB | 2.6 MB | +45.33% |
| ⚡ | test_to_lp[nodal_balance-severity=0] |
3.8 MB | 2.6 MB | +45.27% |
| ⚡ | test_to_lp[storage-n=10] |
1.6 MB | 1.1 MB | +44.9% |
| ⚡ | test_to_lp[expression_arithmetic-n=250] |
49.9 MB | 36.3 MB | +37.44% |
| ⚡ | test_to_lp[piecewise-n=1000] |
1.9 MB | 1.4 MB | +37.3% |
| ⚡ | test_to_lp[basic-n=250] |
29.9 MB | 21.8 MB | +37.15% |
| ⚡ | test_to_lp[sos-n=1000] |
1.4 MB | 1.1 MB | +21.19% |
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/sparse-pipeline (2e33cec) with master (1b2ea76)
Footnotes
-
181 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. ↩
Build cost — v1 vs legacyv1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).
Full table (time + peak, mean)📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run. Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped. |
Note
The following content was generated by AI.
Changes proposed in this Pull Request
Follow-up to #985/#987/#988/#990. An assessment of the sparse (
Model(sparse=True)) data flow from expression construction to the solver found the core hand-off close to optimal (one CSR build per operation, one label remap, a no-copy pass to the direct APIs) but avoidable cost in the stages around it. This PR removes that cost. Every change keeps results bit-identical to master; each was implemented, independently reviewed and revised in its own round.Solver hand-off
test_optimization.py -k mosekon a licensed machine.getModel()/passModel()instead ofgetLp(). Re-passing the LP-only object drops the Hessian, so a QP solved with names on got a wrong objective. A regression test is added.LP writer
Expressions
linopy.mergeof CSR-backed expressions concatenates all operands once instead of folding pairwise, so an N-way merge is linear in total nonzeros rather than quadratic in operand count.LinearExpression.solution/QuadraticExpression.solutionmap the solution by variable label and evaluate CSR-backed expressions on the sparse backing. They no longer rebuild the full constraint matrix nor densify.Model.dualizereadsmodel.matricesonce.Matrix assembly and read-back
model.matricesgathers constraint blocks into preallocated CSR buffers in one pass, withoutvstack,eliminate_zerosor a full-grid scaling round trip; scaling is skipped when identity. Label lookups use the model's label dtype, somatrices.clabelsandmatrices.indicator_binvarareint32by default (behaviour change, noted in the release notes).sanitize_zerosare vectorised without large temporaries.The chunk sizing of the sparse
@contraction was also tried but is redundant after #990 and was reverted.Benchmarks (Model(sparse=True), 8.18M rows, 4.4M variables, 16.7M nonzeros, highspy 1.15, numpy 2.4)
model.matricesassign_result)sanitize_zerosper solvemergeof 8 CSR operands, 8M nnzexpr.solution, CSR expression (40k x 200 vars)expr.solution, dense expression, same modelThe retained outputs of
model.matricesalone are ~555 MB on this model; transient overhead is now ~16 MB.Verification
model.matricescompared value-for-value against master over 25 models (masks, explicit zeros, NaN rhs, scaling, removed containers, integer/binary, indicator, empty).solutioncompared against the dense path with absent cells, NaN solution values and auxiliary coordinates.Checklist
AGENTS.md).doc.doc/release_notes.rstof the upcoming release is included.