Skip to content

perf: configurable float storage dtype (default float32, ~½ coeffs memory)#839

Draft
FBumann wants to merge 3 commits into
PyPSA:masterfrom
fluxopt:perf/values-float32
Draft

perf: configurable float storage dtype (default float32, ~½ coeffs memory)#839
FBumann wants to merge 3 commits into
PyPSA:masterfrom
fluxopt:perf/values-float32

Conversation

@FBumann

@FBumann FBumann commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

DO NOT MERGE

Im exploring other dtype performance gains. Exploratory for now.

In this branch the values default is float32. This needs to be opt-in, bit is default for ci to show the improvements!

Note

The following content was generated by AI.

Follow-up to the int32-labels win (#566). That change halved the integer label/vars arrays; the float arrays were left at float64. In a built model coeffs is the single largest array (2× the vars array), so storing the float arrays as float32 is a comparable memory lever.

What this does. Extends the per-model _dtypes dict (introduced in #828, previously "labels"-only) with a "values" key controlling the storage dtype of the float arrays — coefficients, constants, right-hand sides, and variable lower/upper bounds. Accepts np.float32 or np.float64.

Default. Ships defaulting to np.float32 (opt-out) on purpose, and only for now, so CI memory benchmarks surface the reduction on this PR. The intent is to flip the default to np.float64 (opt-in) before this is considered for merge — float32 coefficients lose precision and can bite badly-scaled / big-M problems, so it should not be the silent default. Model(dtypes={"values": np.float64}) already restores full double-precision storage today.

Implementation & verification
  • linopy/model.py: DtypeKey = Literal["labels", "values"]; annotations widened signedinteger → number; _resolve_dtypes defaults {"labels": int32, "values": float32} and branch-validates per key; add_variables casts bounds to the values dtype; docstrings updated.
  • linopy/expressions.py: the BaseExpression.__init__ chokepoint casts coeffs/const to model._dtypes["values"].
  • linopy/constraints.py: Constraint.__init__ casts rhs to the values dtype.
  • linopy/matrices.py left untouched — derived/solver matrices stay float64 (solvers want double; the win is in storage).
  • The internal vstack in LinearExpression.simplify is deliberately kept float64: it shares a dtype with var-IDs, and float32 would round IDs past 2²⁴. Storage still ends up float32 via the __init__ cast.

Tests (test/test_dtypes.py): default-float32 on bounds/coeffs/const/rhs, float64 override, rejection of float16, and a float32 HiGHS solve to the correct optimum.

  • test/test_dtypes.py: 21 passed
  • test/test_optimization.py (solve subset across gurobi/highs/cbc/mosek/…): 340 passed, 10 skipped
  • expr/constraint/variable/quadratic suites: 601 passed
  • test_io.py + test_matrices.py: 43 passed, 3 skipped
  • mypy clean (only a pre-existing unrelated solvers.py error remains); ruff clean.

Memory sanity. 100k×2-term constraint: coeffs 800000 bytes vs 1600000 at float64 — exactly halved.

🤖 Generated with Claude Code

…fault float32)

Extend the per-model dtypes mechanism (Model._dtypes) with a new "values"
key controlling the storage dtype of the model's float arrays: coefficients,
constants, right-hand sides and variable lower/upper bounds. coeffs is the
single largest array in a built model, so defaulting to float32 roughly
halves peak build memory. Solvers upcast to double internally, so solutions
are unaffected; the LP/MPS writers and direct backends handle float32 fine.

- model.py: DtypeKey gains "values"; annotations widened to type[np.number];
  _resolve_dtypes validates "values" as np.float32/np.float64 (default
  np.float32) and "labels" as before; add_variables casts bounds to the
  values dtype; docstrings updated.
- expressions.py: LinearExpression.__init__ casts coeffs and const to the
  model's values dtype (all expression construction funnels through here,
  including simplify() results).
- constraints.py: Constraint.__init__ casts rhs to the model's values dtype.

Pass Model(dtypes={"values": np.float64}) to restore full float64 storage.
The float32 default is TEMPORARY, to surface the reduction in CI memory
benchmarks, and is intended to flip to opt-in (float64 default) later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 48.67%

⚡ 99 improved benchmarks
✅ 74 untouched benchmarks
⏩ 173 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory test_op[expr_add_scalar] 187.5 KB 93.8 KB ×2
Memory test_op[var_div_scalar] 562.5 KB 281.3 KB +100%
Memory test_op[expr_mul_array_match] 468.8 KB 234.4 KB +100%
Memory test_op[expr_mul_scalar] 468.8 KB 234.4 KB +100%
Memory test_to_lp[expression_arithmetic-n=250] 2.9 MB 1.4 MB +99.3%
Memory test_to_lp[knapsack-n=10000] 82.2 KB 43.9 KB +87.34%
Memory test_to_lp[nodal_balance-severity=0] 129.1 KB 71.3 KB +81.19%
Memory test_op[expr_sum_all] 212.3 KB 118.6 KB +79.05%
Memory test_op[expr_sum_dim] 212.3 KB 118.6 KB +79.05%
Memory test_op[con_eq_expr] 1,079.7 KB 610.9 KB +76.73%
Memory test_op[expr_sub_expr_match] 1,079.7 KB 610.9 KB +76.73%
Memory test_op[var_add_var_match] 1,079.9 KB 611.1 KB +76.71%
Memory test_op[var_sub_var_match] 1,079.9 KB 611.1 KB +76.71%
Memory test_op[expr_add_var] 986 KB 564.1 KB +74.79%
Memory test_op[var_add_var_bcast] 4.4 MB 2.6 MB +73.17%
Memory test_op[expr_add_expr_bcast] 4.4 MB 2.5 MB +72.68%
Memory test_op[var_mul_var] 891.8 KB 516.8 KB +72.57%
Memory test_op[expr_add_expr_match] 892.1 KB 517.1 KB +72.52%
Memory test_op[expr_add_masked] 892.1 KB 517.1 KB +72.52%
Memory test_build[cumsum-severity=0] 16.4 KB 9.6 KB +70.24%
... ... ... ... ... ...

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

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fluxopt:perf/values-float32 (d23e0d7) with master (c2607b6)

Open in CodSpeed

Footnotes

  1. 173 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.

FBumann and others added 2 commits July 16, 2026 13:48
CI's warn_unused_ignores flags the two # type: ignore[dict-item] on the
invalid-dtype rejection tests as unnecessary; np.float64/np.float16 are
valid type[np.number] values (the rejection is a runtime check).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multiplying/adding a float64 array against a float32 expression let numpy
upcast to a float64 broadcast intermediate that __init__ then downcast
back to float32 — a large transient that raised op peak memory ~17-27%
over master (CodSpeed test_op[expr_{mul,add}_array_bcast]).

Cast the other operand to the model value dtype first (extracted as
_as_value_dtype), so the elementwise op stays float32 end to end. Peak
now drops below master (mul_array_bcast 1.04MB -> 0.69MB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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