Skip to content

perf(v1): skip mask/where/const for dense variables in to_linexpr#836

Merged
FBumann merged 1 commit into
feat/arithmetic-conventionfrom
perf/v1-dense-var-mul
Jul 16, 2026
Merged

perf(v1): skip mask/where/const for dense variables in to_linexpr#836
FBumann merged 1 commit into
feat/arithmetic-conventionfrom
perf/v1-dense-var-mul

Conversation

@FBumann

@FBumann FBumann commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

TODO: your own intent/motivation here.

Note

The following content was generated by AI.

What

Variable.to_linexpr under v1 unconditionally built an absent mask, a
where(~absent) copy, and an explicit full-size all-zero const array —
even for a fully-dense variable with no absent slots. On the isolated
var * array (exact-match) path these are all no-ops but still allocate
full-size temporaries.

This is the source of the remaining CodSpeed memory regression after #804/#816:

Mode Benchmark BASE HEAD Δ
Memory test_op[var_mul_array_match] 212.3 KB 306.1 KB −30.6%

and the small-model test_build peaks (knapsack-100 +19%, milp-10 +17%,
sparse_network-10 +16%, sos-10 +13%). Large models were already at parity —
the all-zero const is pruned at matrix assembly (#816), so built models
are byte-identical; the regression only shows on the isolated expression object.

Fix

Gate the mask / where / const on has_absence = (labels == -1).any().
Dense variables now build the same lean coeffs+vars expression as legacy;
masked variables still carry NaN at absent slots for §6 propagation.

Measurements

Isolated var * array peak (memray, warmed):

n legacy v1 before v1 after
5 000 123.9 KiB 133.7 KiB 78.1 KiB
20 000 490.1 KiB 529.2 KiB 312.5 KiB
200 000 4884.7 KiB 5275.3 KiB 3125.0 KiB

v1 now sits below legacy on this path.

Tests

Full suite passes under both semantics: 7813 passed, 605 skipped
(pytest test/). Dense/masked equivalence verified: masked coeffs
still [0, nan, 2, nan].

Follow-up (not in this PR)

The v1 branch still runs reindex_like(fill_value=nan) even when the
coefficient already matches labels exactly (first_mismatched_dim returned
None). Skipping that no-op copy — mirroring _align_constant's
needs_data_reindex=False path from #804 — would trim further.

Variable.to_linexpr under v1 unconditionally built an `absent` mask, a
`where(~absent)` copy and an explicit all-zero `const` array, even for a
fully-dense variable that carries no absent slots. On the isolated
`var * array` (match) path those are all no-ops but still allocate
full-size temporaries — the CodSpeed test_op[var_mul_array_match] memory
regression (212 -> 306 KB) and the small-model test_build peaks.

Gate the mask/where/const on `has_absence`. Dense variables now build the
same lean coeffs+vars expression as legacy; masked variables still carry
NaN at absent slots for §6 propagation. Isolated var*array peak (memray):
v1 5000-elem 133.7 -> 78.1 KiB, 200k 5275 -> 3125 KiB (now below legacy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FBumann
FBumann merged commit 6d995fb into feat/arithmetic-convention Jul 16, 2026
1 of 2 checks passed
@FBumann
FBumann deleted the perf/v1-dense-var-mul branch July 16, 2026 07:59
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.

1 participant