Skip to content

perf: store Solution attributes in slots and tighten the memory gate - #5827

Merged
BradyPlanden merged 6 commits into
mainfrom
a2/solution-compact-attributes
Oct 1, 2026
Merged

BradyPlanden merged 6 commits into
mainfrom
a2/solution-compact-attributes

Conversation

@BradyPlanden

Copy link
Copy Markdown
Member

Description

Stacked on #5782, whose Memory tests job failed test_cycle_count_memory_scaling with a 20-to-5 cycle memory ratio of 1.403 against a 1.4 gate.

Profiling with tracemalloc on Python 3.13.15 Linux (x86_64 and aarch64, with CI's pybammsolvers wheels) showed that the solver statistics #5782 adds cost 72 B per step, one SolverStatistics. That is within run-to-run noise: main and #5782 both measured 1.39–1.40, so the gate was already at the edge on main. Most of each step's memory was Solution overhead. An experiment keeps four solutions per step (the step, its cycle, and the cycle's cached first and last states), and each carried a full-size __dict__ (1,584 B on 3.13). Solution set more instance attributes than CPython's 30-key shared layout holds, and Python 3.14 pre-fills those keys from __static_attributes__. Whether a solution's attributes fit therefore also depended on which solutions the process had made before, so the test's ratio moved with test order.

  • Solution and SolutionBase declare __slots__, so every solution gets the same compact layout whatever the Python version or process history. __dict__ stays, so callers can still attach their own attributes.
  • first_state and last_state cache in slots instead of through functools.cached_property, which needs __dict__.
  • Solution.__setstate__ loads solutions pickled before this change: a plain dict state, without solver_statistics, with the cached states under their property names.
  • test_cycle_count_memory_scaling's gate drops from 1.4 to 1.3, so a regression to main's level fails it.
  • An editable pybammsolvers install, which uv sync creates, can be imported on its own. idaklu's only CasADi rpath was ../casadi, which holds for a wheel but not for the build tree an editable install loads from, so import pybammsolvers failed to load libcasadi unless casadi had been imported first. Editable builds also search the environment's CasADi directory; wheels are unchanged.

Run after the rest of the memory suite, as CI runs it:

Platform Ratio Growth per 4-step cycle
Linux x86_64, Python 3.13 (CI) 1.242–1.245 32.9 KB, from 55.9 KB on #5782
Linux aarch64, Python 3.13 1.240–1.253 33.1 KB
macOS arm64, Python 3.14 1.265–1.279 37.4 KB

Type of change

Add an entry under # [Unreleased] in CHANGELOG.md, in one of ## Breaking changes, ## Deprecated, ## Features, or ## Bug fixes (include PR number; breaking and deprecation entries need a one-line migration note). Internal-only PRs — refactor, docs, CI, tests — can skip this. See RELEASE.md for the full policy.

Important checks:

Please confirm the following before marking the PR as ready for review:

  • No style issues: nox -s pre-commit
  • All tests pass: nox -s tests
  • The documentation builds: nox -s doctests
  • Code is commented for hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@BradyPlanden
BradyPlanden requested a review from a team as a code owner October 1, 2026 09:19
BradyPlanden added a commit that referenced this pull request Oct 1, 2026
@BradyPlanden
BradyPlanden added this pull request to stack #5828 October 1, 2026 09:20
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

ProjectPyBaMM
Brancha2/solution-compact-attributes
Testbedbare-metal

⚠️ WARNING: Truncated view!

The full continuous benchmarking report exceeds the maximum length allowed on this platform.

🐰 View full continuous benchmarking report in Bencher

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.09%. Comparing base (b3c168f) to head (423ed38).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5827   +/-   ##
=======================================
  Coverage   98.09%   98.09%           
=======================================
  Files         346      346           
  Lines       35323    35354   +31     
=======================================
+ Hits        34649    34680   +31     
  Misses        674      674           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@martinjrobins martinjrobins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to remove the dict if we are using slots? I don't think anyone else should be adding attributes to this class

@martinjrobins martinjrobins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @BradyPlanden !

Base automatically changed from a2/idaklu-stats-scheduling to main October 1, 2026 14:08
An experiment keeps four solutions per step (the step, its cycle and the
cycle's first and last states), and each carried a full-size __dict__.
Solution set more instance attributes than CPython's 30-key shared
layout holds, and Python 3.14 pre-fills those keys from
__static_attributes__, so the layout also depended on which solutions a
process had made before. Slots give every solution the same compact
layout. first_state and last_state cache in slots instead of through
cached_property, __dict__ stays for attributes callers attach, and
__setstate__ loads solutions pickled before Solution had slots.
idaklu's only CasADi rpath is ../casadi, which holds for a wheel beside
casadi in site-packages but not for an editable install, whose extension
lives in the build tree. import pybammsolvers then failed to load
libcasadi unless casadi had been imported first. An editable build also
searches the environment's CasADi directory.
With solutions in slots the 20-to-5 cycle ratio is about 1.25 on Python
3.13 and 1.28 on 3.14 when run after the rest of the memory suite, so a
1.4 gate no longer caught a regression the size of main's 1.39.
SolutionBase's slots took _data, set_up_time and solve_time out of the
instance dict, but only Solution restored a pre-slots pickle. An older
EISSolution put them in __dict__, where the slots hid them, so its
impedance, frequencies, data and total_time raised AttributeError.
SolutionBase now restores every pickled attribute through setattr, and
Solution keeps only its own defaults and renamed state caches.
@BradyPlanden
BradyPlanden force-pushed the a2/solution-compact-attributes branch from 7dd26c8 to 423ed38 Compare October 1, 2026 14:08
@BradyPlanden
BradyPlanden merged commit 65a027a into main Oct 1, 2026
44 checks passed
@BradyPlanden
BradyPlanden deleted the a2/solution-compact-attributes branch October 1, 2026 16:12
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.

3 participants