Skip to content

perf: dense fast paths in the core and a faster spec build - #1078

Merged
FabianHofmann merged 6 commits into
masterfrom
perf/dense-fast-paths
Oct 28, 2026
Merged

FabianHofmann merged 6 commits into
masterfrom
perf/dense-fast-paths

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Note

The following content was generated by AI.

Follow-up to #958.

Changes proposed in this Pull Request

Speeds up model building on the dense path, both for linopy's core and for Model.from_spec.

Core (dense store, expressions, model)

  • DenseStore / merge gain fast paths that skip xarray alignment, Dataset construction and copies when operands already share their coordinates.
  • Variable to expression conversion, constant arithmetic, where and subtraction on LinearExpression use these paths.
  • add_variables and add_constraints allocate labels and check infinities and masks in numpy.
  • on_coords in linopy/store.py builds a dataset from fields on given coordinates with the public Dataset constructor, reusing the coordinates' indexes. The dataset dims follow the field layout, so expr.expand_dims("new").coord_dims now lists new first.

Spec builder

  • Evaluated nodes are cached within a build. References and named-expression bodies stay cached; other data nodes only while a later use remains. Terms are never cached, to keep peak memory down.
  • A constraint whose where: keeps no row is skipped before its sides are evaluated. It adds no constraint group and reserves no labels. Its dual(...) reads as all NaN.
  • sum(..., by=...) on a dense linear expression groups in numpy. The result is identical to the groupby().sum() path, including dim and term order.
  • Relation columns and row masks are cached on the relation table.

Behaviour change: fully masked spec constraints no longer appear in model.constraints, and later constraints are numbered consecutively. On the PyPSA spec the LP differs from the previous output only in constraint row numbers. This was checked by mapping old labels to new ones.

Results (PyPSA spec, minimum build time)

Case before after PyPSA create_model, same core
small (n=2) 7.1 s 1.27 s 0.43 s
large dense (T=2000, N=50, 18.6 M cons) 10.85 s, 3.19 GB 2.86 s, 2.20 GB 2.20 s, 1.69 GB

PyPSA's own create_model also builds about twice as fast on this core (0.87 s → 0.44 s small). The remaining gap to PyPSA comes from the spec's larger number of constraint groups (122 vs 24) and per-operation xarray overhead.

Core memory. The first push of this PR raised peak memory on the core micro-benchmarks, e.g. kvl_cycles 87.7 MB → 121.5 MB. The cause was Variable._dense_term. It built const and coeffs with np.where even when no label was absent, where master kept stride-0 broadcast views until the next reduction. It now keeps the views when no label is absent:

Benchmark master first push now
kvl_cycles build 87.8 MB, 41 ms 121.5 MB, 34 ms 44.6 MB, 19 ms
sparse_network n=250 26.6 MB, 34 ms 36.6 MB, 15 ms 18.3 MB, 10 ms
var_mul_array_bcast 138 KB, 3.2 ms 1086 KB, 1.1 ms 148 KB, 1.1 ms

(local tracemalloc peak and best-of-5 time)

A constraint built as 2 * x >= 4 now holds read-only coefficient arrays, as master already did for broadcast cases such as a * x with a new dim or (flow * C).sum(...). Raw in-place writes like c.coeffs.values[...] = ... stay unsupported and now raise there. test_preview_detects_raw_mutation_apply_skips_it replaces the array instead of writing into it.

test/test_repr.py now builds the transposed-dataset fixture with integer coefficients. That keeps it on the generic path the test is about, rather than the new float fast path.

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

Store/expression/model fast paths skip alignment and copies on shared coordinates.
from_spec reuses evaluated nodes, skips constraints whose where keeps no row,
and groups dense sum-by in numpy.
@codspeed

codspeed Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by ×2.7

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

⚡ 147 improved benchmarks
✅ 215 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
⚡ Walltime test_op[var_div_scalar] 36.9 ms 2.2 ms ×17
⚡ Memory test_op[var_neg] 199.2 KB 12.6 KB ×16
⚡ Memory test_op[var_to_linexpr] 199.2 KB 12.6 KB ×16
⚡ Walltime test_op[var_neg] 17 ms 1.1 ms ×15
⚡ Walltime test_op[var_to_linexpr] 16.9 ms 1.1 ms ×15
⚡ Walltime test_op[expr_sub_expr_match] 33.2 ms 2.2 ms ×15
⚡ Walltime test_op[var_mul_scalar] 16.8 ms 1.1 ms ×15
⚡ Walltime test_op[expr_where] 17 ms 1.2 ms ×14
⚡ Walltime test_op[var_add_scalar] 27.1 ms 2.1 ms ×13
⚡ Walltime test_op[var_add_array_match] 31.1 ms 2.5 ms ×13
⚡ Walltime test_op[var_sub_var_match] 56.5 ms 4.6 ms ×12
⚡ Walltime test_op[var_add_var_match] 56.6 ms 4.6 ms ×12
⚡ Walltime test_op[expr_add_var] 39.6 ms 3.3 ms ×12
⚡ Walltime test_op[var_sum_dim] 26.8 ms 2.3 ms ×12
⚡ Walltime test_op[expr_add_scalar] 10,118.5 µs 883.2 µs ×11
⚡ Walltime test_op[expr_mul_array_match] 16.5 ms 1.5 ms ×11
⚡ Walltime test_op[con_le_scalar] 37.4 ms 3.3 ms ×11
⚡ Walltime test_op[expr_add_array_match] 14.2 ms 1.3 ms ×11
⚡ Walltime test_op[expr_mul_scalar] 10,445.5 µs 993.9 µs ×11
⚡ Walltime test_op[expr_add_expr_match] 22.6 ms 2.3 ms ×9.7
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

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/dense-fast-paths (5a35080) with master (8189160)

Open in CodSpeed

on_cells, laid_on, transposed, coords_within, DenseStore._own and
DenseStore.normalized replace the per-site copies; to_constraint moves
its numpy step into DenseStore.moved. Drop the unreachable helper-dim
guard in where and tune the _stacked slab copy to two terms. Document
the three dense-store conventions in contributing.rst.
A scalar sign, scaling or rhs on a constraint grid is a stride-0 view
again, as on master, rather than a filled array. The dense
to_constraint computes the moved constant once instead of negating a
copy. Peak memory of con_le_array drops from 299 KB to 138 KB locally.
@FabianHofmann
FabianHofmann merged commit b1d6a2e into master Oct 28, 2026
22 checks passed
@FabianHofmann
FabianHofmann deleted the perf/dense-fast-paths branch October 28, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant