perf: sparse-aware dot/matmul against a mostly-zero constant#867
perf: sparse-aware dot/matmul against a mostly-zero constant#867FBumann wants to merge 2 commits into
Conversation
(self * other).sum(common_dims) stacks one term per element of the contracted dimensions regardless of zeros in the operand, so a sparse matrix (e.g. a cycle incidence) densifies the result. Pair each nonzero entry of the operand with its slice of the expression instead and group-sum the entries, so only contributing terms are materialized. The kernel engages when the operand's nonzero density is at most linopy.options['sparse_dot_max_density'] (default 0.5, 0 disables) and the contracted labels already match the expression exactly; every other case falls back to the dense path unchanged, so no alignment semantics are re-implemented. KVL-shaped contraction (852 branches, 268 cycles, 24 snapshots, 3 branches/cycle): result term dimension 852 -> 3, coeff+var cells 10,960,128 -> 38,592, memray process peak 282 MB -> 97 MB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merging this PR will not alter performance
Performance Changes
Comparing Footnotes
|
|
Note AI-generated note (Claude Code, prompted by @FBumann). Benchmark coverage: #868 adds a matmul op group to |
The suite had no benchmark exercising @/dot: the kvl_cycles pattern
builds its constraint via the expanded (flow * C).sum('branch'), which
bypasses __matmul__ entirely, and ops.py had no contraction op. A
sparse-aware matmul kernel (#748/#867) would land invisible to CI.
Add a matmul op group contracting the profile's large dim against a
(1000 x 100) constant: expr_matmul_dense (fully nonzero — stays on the
dense kernel) and expr_matmul_sparse (incidence-shaped, ~3 nonzeros per
column — the case a sparse-aware kernel collapses). New ops carry no
CodSpeed history, so this changes no existing baseline; kvl_cycles is
left untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite had no benchmark exercising @/dot: the kvl_cycles pattern
builds its constraint via the expanded (flow * C).sum('branch'), which
bypasses __matmul__ entirely, and ops.py had no contraction op. A
sparse-aware matmul kernel (#748/#867) would land invisible to CI.
Add a matmul op group contracting the profile's large dim against a
(1000 x 100) constant: expr_matmul_dense (fully nonzero — stays on the
dense kernel) and expr_matmul_sparse (incidence-shaped, ~3 nonzeros per
column — the case a sparse-aware kernel collapses). New ops carry no
CodSpeed history, so this changes no existing baseline; kvl_cycles is
left untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite had no benchmark exercising @/dot: the kvl_cycles pattern
builds its constraint via the expanded (flow * C).sum('branch'), which
bypasses __matmul__ entirely, and ops.py had no contraction op. A
sparse-aware matmul kernel (#748/#867) would land invisible to CI.
Add a matmul op group contracting the profile's large dim against a
(1000 x 100) constant: expr_matmul_dense (fully nonzero — stays on the
dense kernel) and expr_matmul_sparse (incidence-shaped, ~3 nonzeros per
column — the case a sparse-aware kernel collapses). New ops carry no
CodSpeed history, so this changes no existing baseline; kvl_cycles is
left untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite had no benchmark exercising @/dot: the kvl_cycles pattern
builds its constraint via the expanded (flow * C).sum('branch'), which
bypasses __matmul__ entirely, and ops.py had no contraction op. A
sparse-aware matmul kernel (#748/#867) would land invisible to CI.
Add a matmul op group contracting the profile's large dim against a
(1000 x 100) constant: expr_matmul_dense (fully nonzero — stays on the
dense kernel) and expr_matmul_sparse (incidence-shaped, ~3 nonzeros per
column — the case a sparse-aware kernel collapses). New ops carry no
CodSpeed history, so this changes no existing baseline; kvl_cycles is
left untouched.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Note
The following content was generated by AI (Claude Code), prompted and reviewed by @FBumann.
Closes #748.
LinearExpression.__matmul__is(self * other).sum(common_dims), which stacks one term per element of the contracted dimensions regardless of zeros in the operand — a sparse constant (e.g. PyPSA's cycle-incidence matrix in the KVL constraint) densifies the result to full_term(284× more cells than needed, the largest single allocation in a SciGRID-DE build, see #748/#749).This adds a sparse-aware kernel using the reframe from the issue: each nonzero entry of the operand belongs to exactly one output group, so the contraction is a clean sparse-entry
groupby(free_dim).sum(). The kernel selects the nonzero entries (iselwith a flat entry dimension), multiplies each with its slice of the expression, and group-sums via the existing scatter kernel (#802). Only contributing terms are materialized; the represented expression is identical to the dense path's — only the positional term layout (count, order, padding) differs, which linopy already treats as non-semantic (densify_terms, zero-coeff filtering at export).When it engages (all other cases fall back to the dense path unchanged):
linopy.options["sparse_dot_max_density"](new option, default0.5;0disables the sparse path),NaN entries in the operand are kept as terms (
!= 0), matching the dense path's NaN propagation under both current and future (v1) semantics. The constant part is reduced with the same skipna semantics as_sum.Benchmark (memray, KVL-shaped contraction: 852 branches × 268 cycles, 24 snapshots, 3 branches/cycle)
Op-only measurement (
memray.Trackeraround just theexpr @ Ccall, native traces):_termsparse_dot_max_density=0)284× fewer result cells (matching the figures in #748), ~48× lower op peak. Whole-process peak (interpreter + model build + op) drops 282.3 MB → 97.1 MB.
Tests: equivalence against the dense path via a canonical (term-layout-insensitive) comparison — incidence matrix, vector contraction, expression with constant, NaN entries, aux coords on the free dim, coordinate-less free dim — plus fallback tests (reordered labels, dense operand, duplicate free labels, >1 free dims, option disabled) asserting the dense path's exact layout is unchanged. Full suite: 3800 passed, 45 skipped.
Sibling issues #745/#749/#756 (groupby padding, ragged merge, long-format umbrella) are not touched; this is the self-contained first step of the #756 plan.
🤖 Generated with Claude Code