Skip to content

Use mimalloc as the Python allocator instead of preloading jemalloc - #175604

Merged
agners merged 1 commit into
home-assistant:devfrom
agners:mimalloc-python-allocator
Jul 13, 2026
Merged

Use mimalloc as the Python allocator instead of preloading jemalloc#175604
agners merged 1 commit into
home-assistant:devfrom
agners:mimalloc-python-allocator

Conversation

@agners

@agners agners commented Jul 4, 2026

Copy link
Copy Markdown
Member

Breaking change

This is not a breaking change for configuration. One behavior note for transparency: the DISABLE_JEMALLOC environment variable no longer has any effect (jemalloc is no longer preloaded) and now logs a warning. Users who set it — most commonly on aarch64 systems with 16 KB/64 KB memory pages, where preloaded jemalloc aborted with Unsupported system page size — no longer need it, because mimalloc does not have that incompatibility. To fall back to CPython's default allocator, set PYTHONMALLOC=default or PYTHONMALLOC=pymalloc explicitly.

Proposed change

Switch the Home Assistant Core container from preloading jemalloc process-wide to selecting mimalloc as CPython's object allocator via PYTHONMALLOC=mimalloc.

In rootfs/etc/services.d/home-assistant/run:

-# Enable mimalloc for Home Assistant Core, unless disabled
-if [[ -z "${DISABLE_JEMALLOC+x}" ]]; then
-  export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
-  export MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000"
-fi
+# Use mimalloc as Python's object allocator by default. It is bundled in CPython
+# (3.13+), so no LD_PRELOAD or extra library is required, and it uses noticeably
+# less memory than the previously preloaded jemalloc. Override or disable via
+# PYTHONMALLOC, e.g. `PYTHONMALLOC=pymalloc` for the default allocator.
+export PYTHONMALLOC="${PYTHONMALLOC:-mimalloc}"
+
+if [[ -n "${DISABLE_JEMALLOC+x}" ]]; then
+  bashio::log.warning "DISABLE_JEMALLOC is set but no longer has any effect: jemalloc has been replaced by mimalloc. Set PYTHONMALLOC=pymalloc to use the default allocator instead."
+fi

PYTHONMALLOC=mimalloc uses the mimalloc that is already bundled in CPython 3.13+ (--with-mimalloc, enabled by default), so no LD_PRELOAD and no external allocator library are needed. PYTHONMALLOC stays fully user-overridable, so it doubles as the opt-out (PYTHONMALLOC=pymalloc) and replaces the single-purpose DISABLE_JEMALLOC flag.

Motivation

jemalloc was originally adopted to work around the slow musl allocator, as documented in the 2020 developer blog post: https://developers.home-assistant.io/blog/2020/07/13/alpine-python/. That was measured against musl's old allocator; musl has since shipped mallocng (default since musl 1.2.1), and the picture has changed.

jemalloc no longer earns its keep here, and it carries a real portability hazard:

  1. Page-size incompatibility on aarch64. jemalloc bakes the page size in at build time. A build assuming 4 KB pages aborts with <jemalloc>: Unsupported system page size on kernels using 16 KB (Raspberry Pi 5, Apple Silicon) or 64 KB (Ampere, some RHEL/Fedora arm64) pages. This is exactly why the DISABLE_JEMALLOC escape hatch exists, and the same class of crash is widely reported across projects (Polars, Qdrant, Typesense, KeyDB, Reth, and others). mimalloc queries the page size at runtime (sysconf(_SC_PAGESIZE) in _mi_prim_mem_init()), so it adapts to 4/16/64 KB automatically — no compiled-in assumption, and no such crash reports exist against it. Switching to mimalloc removes the reason DISABLE_JEMALLOC was needed.

  2. Memory. Benchmarking the current image (Python 3.14, musl, GCC/full-LTO/PGO) shows jemalloc retains substantially more memory than either plain musl or mimalloc, for no measurable CPU benefit.

Benchmark results

CPU measured with the pyperformance suite (122 benchmarks — the same tool used in the 2020 blog); peak RSS measured with Core's built-in benchmark workload. All on the qemux86-64 image, CPU-pinned, warmed up.

Allocator config CPU (pyperformance geomean) peak RSS
jemalloc (current default) reference reference
plain musl + pymalloc ±0% (no measurable difference) −30%
PYTHONMALLOC=mimalloc (this PR) −1.2% −27%
mimalloc process-wide (LD_PRELOAD) −1.1% +3% to +9%

Two takeaways: jemalloc shows no measurable CPU advantage over plain musl while using ~40% more peak RSS, and PYTHONMALLOC=mimalloc gives the best combined result — mimalloc's full memory benefit (~27% lower peak RSS than jemalloc) at CPU parity-or-better, without LD_PRELOAD or an external library. Process-wide mimalloc via LD_PRELOAD was tested too and was notably worse on memory, so this PR deliberately uses only the PYTHONMALLOC path.

Supporting references

Caveats / where reviewer input is welcome

  • The benchmarks above are largely single-threaded. jemalloc's and mimalloc's signature advantage is multi-threaded allocation (per-thread caches reducing lock contention), which neither pyperformance nor the internal benchmarks fully exercise. Core runs a thread-pool executor and many integration threads, so real-world concurrent allocation may differ. This is the main thing worth validating — e.g. on the beta channel, or with a real startup + steady-state RSS profile.
  • The jemalloc MALLOC_CONF retention tuning (dirty/muzzy_decay_ms) is dropped; mimalloc's defaults are used. If needed, mimalloc's MIMALLOC_PURGE_DELAY is the analog (higher value = hold freed memory longer, trading RSS for fewer purge syscalls).

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Possible follow-ups, kept out of this PR to stay focused: drop the now-unused jemalloc apk package from the image (size reduction), and add a build/CI assertion that PYTHONMALLOC=mimalloc python3 -c '' succeeds so a future base image cannot silently regress to musl.

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Select mimalloc via PYTHONMALLOC=mimalloc instead of LD_PRELOAD-ing jemalloc.
mimalloc is bundled in CPython 3.13+, so no extra library or LD_PRELOAD is
needed, and it uses noticeably less peak RSS than jemalloc with no measurable
CPU regression on the pyperformance suite.

PYTHONMALLOC stays user-overridable (e.g. PYTHONMALLOC=pymalloc for the default
allocator). Warn when the legacy DISABLE_JEMALLOC variable is set, since it no
longer has any effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 4, 2026 11:09
@home-assistant home-assistant Bot added cla-signed code-quality small-pr PRs with less than 30 lines. labels Jul 4, 2026

Copilot AI 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.

Pull request overview

This PR changes how the Home Assistant Core container selects its memory allocator. Instead of preloading jemalloc process-wide via LD_PRELOAD (with a MALLOC_CONF tuning string and a DISABLE_JEMALLOC opt-out), it now selects CPython's bundled mimalloc via PYTHONMALLOC=mimalloc. The motivation is to avoid jemalloc's build-time page-size assumption (which aborts on 16 KB/64 KB-page aarch64 systems and is the reason DISABLE_JEMALLOC existed) and to reduce peak RSS, while keeping the allocator user-overridable through PYTHONMALLOC. The now-inert DISABLE_JEMALLOC variable emits a warning pointing users to PYTHONMALLOC=pymalloc.

Changes:

  • Replace the jemalloc LD_PRELOAD/MALLOC_CONF block with export PYTHONMALLOC="${PYTHONMALLOC:-mimalloc}", keeping it user-overridable.
  • Add a startup warning when the deprecated DISABLE_JEMALLOC variable is still set, directing users to the PYTHONMALLOC opt-out.

Comment on lines +8 to +11
# Use mimalloc as Python's object allocator by default. It is bundled in CPython
# (3.13+), so no LD_PRELOAD or extra library is required, and it uses noticeably
# less memory than the previously preloaded jemalloc. Override or disable via
# PYTHONMALLOC, e.g. `PYTHONMALLOC=pymalloc` for the default allocator.
@agners

agners commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Full table of benchmarks I've run to evaluate the situation with Python 3.14

Config pyperf vs jemalloc pyperf vs musl json_serialize peak RSS
jemalloc (current HA default) +0.0% 3.940 s 4918 MB (highest)
musl / pymalloc (default) −0.0% 3.974 s 3439 MB (−30%)
mimalloc PYTHONMALLOC-only −1.2% −1.1% 3.865 s 3570 MB (−27%)
mimalloc FULL +0.9% +1.0% 3.612 s 3575 MB (−27%)
mimalloc LD_PRELOAD-only 10 ms −1.1% −1.0% 4.313 s 5087 MB (+3%)
mimalloc LD_PRELOAD-only 20 s −1.4% −1.4% 3.603 s 5365 MB (+9%)

What the columns mean

  • pyperf vs jemalloc / vs musl — geometric mean of per-benchmark run times across the
    pyperformance suite (122 benchmarks; dulwich_log excluded as it fails to build under musl),
    expressed as the ratio to that baseline. Negative = faster than the baseline, positive = slower.
    pyperformance is the same tool used in the 2020 jemalloc blog post.
  • json_serialize — median wall-clock of Home Assistant Core's built-in json_serialize_states
    benchmark (python -m homeassistant --script benchmark), an allocation-heavy state-serialization
    workload. Lower = faster. (warmup = 3, n = 15.)
  • peak RSS — peak resident memory (ru_maxrss) of the process running Core's benchmark driver;
    percentage is relative to jemalloc. Lower = less memory.

How the configs were set

  • jemallocLD_PRELOAD=libjemalloc.so.2 + MALLOC_CONF=…decay_ms:20000… (current HA setup).
  • musl / pymalloc — no allocator override (plain musl malloc + CPython pymalloc).
  • PYTHONMALLOC-onlyPYTHONMALLOC=mimalloc (mimalloc for Python objects; system malloc = musl).
  • FULLLD_PRELOAD=libmimalloc + PYTHONMALLOC=mimalloc (mimalloc everywhere).
  • LD_PRELOAD-only 10 ms / 20 sLD_PRELOAD=libmimalloc only (pymalloc for objects), with
    MIMALLOC_PURGE_DELAY at the default 10 ms vs 20000 ms.

Environment & caveats

  • qemux86-64 image, CPython 3.14.6 (musl, GCC, full-LTO + PGO), container CPU-pinned to 2 cores,
    warmed up (initial runs discarded). Allocator env replicated manually because the benchmark
    bypasses the s6 run script.
  • The pyperformance geomean spread is ~1–2%, which is near the between-run noise floor — read the
    sub-2% rows as "effectively tied, slightly faster than jemalloc," not a strict ranking. The
    robust, large-margin facts are the RSS differences and jemalloc showing no CPU gain over musl.
  • peak RSS is a synthetic high-water mark dominated by the json_serialize workload; the ratios
    are meaningful but the absolute MB values are not production idle figures.
  • Both benchmark suites are largely single-threaded, so they under-represent multi-threaded
    allocation contention (where jemalloc/mimalloc normally help most).

@agners
agners requested a review from bdraco July 4, 2026 11:32
@bluetoothbot

Copy link
Copy Markdown

PR Review — Use mimalloc as the Python allocator instead of preloading jemalloc

Sound, well-motivated allocator change; one robustness gap (no fallback if a build lacks mimalloc) is worth closing before merge.

Strengths worth calling out:

  • Correct, idiomatic shell: ${PYTHONMALLOC:-mimalloc} keeps the setting fully user-overridable and doubles as the opt-out, cleanly replacing the single-purpose DISABLE_JEMALLOC.
  • The deprecated DISABLE_JEMALLOC is handled gracefully — inverted guard + bashio::log.warning pointing users to PYTHONMALLOC=pymalloc, rather than silently ignored.
  • Genuinely fixes the aarch64 16 KB/64 KB page-size abort (runtime page-size query vs jemalloc's compile-time assumption), backed by real pyperformance + Core benchmark data.
  • Self-contained: grep confirms run is the only file in this repo referencing jemalloc, so no dangling docs/config references.

What needs attention:

  • warning: PYTHONMALLOC=mimalloc is a fatal startup error on a Python built without mimalloc — no graceful fallback, unlike the old LD_PRELOAD path. The CI/build assertion is deferred to a follow-up; consider landing the guard now.
  • suggestion: trim the four-line comment (matches Copilot's note and repo comment conventions).

🟡 Important

1. PYTHONMALLOC=mimalloc is a hard, fatal dependency on a mimalloc-enabled build with no runtime fallback
rootfs/etc/services.d/home-assistant/run:10

PYTHONMALLOC=mimalloc only works if the interpreter was compiled with mimalloc support (--with-mimalloc). If a future base image ships a CPython that was built without mimalloc (it is auto-disabled at build time on unsupported configurations), CPython treats this not as a soft fallback but as a fatal startup error — the interpreter prints Fatal Python error: ... mimalloc is not enabled and exits before Home Assistant loads.

Why it matters: the previous LD_PRELOAD scheme degraded gracefully — a missing/incompatible jemalloc still let Core boot (that was the whole point of DISABLE_JEMALLOC). This change removes that safety margin: a silent base-image regression takes down every install at startup, not just slows it. That is a strictly worse failure mode than the page-size crash this PR fixes.

The PR body acknowledges this and defers the guard to a follow-up ("add a build/CI assertion that PYTHONMALLOC=mimalloc python3 -c '' succeeds"). That is the right mitigation, but it is deferred while the hard dependency ships now.

Suggested options (pick one):

  • Land the CI/build assertion in this PR rather than as a follow-up, so a mimalloc-less base image fails the build instead of production.
  • Or make the run script self-healing, e.g. probe once and fall back:
if [[ -z "${PYTHONMALLOC+x}" ]] && python3 -c 'import sys' PYTHONMALLOC=mimalloc 2>/dev/null; then
  export PYTHONMALLOC=mimalloc
fi

(the exact probe needs refinement, but the intent is: only force mimalloc if the interpreter actually supports it).

Not blocking given you have verified it works on the current 3.14 musl build, but the loss of graceful degradation deserves an explicit guard.

export PYTHONMALLOC="${PYTHONMALLOC:-mimalloc}"

🟢 Suggestions

1. Trim the four-line comment to the non-obvious constraint
rootfs/etc/services.d/home-assistant/run:8-11

Agreeing with @Copilot here, and it aligns with this repo's comment conventions (state the why / non-obvious constraint, concisely). The four lines partly restate the PR description and reference the prior jemalloc implementation, which will read as stale history once jemalloc is gone.

A single line carries the load-bearing fact:

# mimalloc (bundled in CPython 3.13+) as Python's object allocator; override with PYTHONMALLOC=pymalloc.
export PYTHONMALLOC="${PYTHONMALLOC:-mimalloc}"

Minor — take it or leave it.


Checklist

  • Allocator selection is user-overridable and opt-out preserved
  • Deprecated variable handled with clear migration warning
  • No dangling references to removed jemalloc setup
  • Graceful degradation / fallback on unsupported build — warning #1
  • Comments concise, state why not what — suggestion #1

To rebase specific severity levels, mention me: @bluetoothbot rebase critical (fixes 🔴 only), @bluetoothbot rebase important (fixes 🔴 + 🟡), or just @bluetoothbot rebase for all.


Automated review by Kōan (Claude) HEAD=8865d92 1 min 57s

@bluetoothbot bluetoothbot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issues found.

  • PYTHONMALLOC=mimalloc is a hard, fatal dependency on a mimalloc-enabled build with no runtime fallback

@agners

agners commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Blocking issues found.

* PYTHONMALLOC=mimalloc is a hard, fatal dependency on a mimalloc-enabled build with no runtime fallback

We maintain our base images, so this doesn't seem a blocker to me.

@bdraco

bdraco commented Jul 7, 2026

Copy link
Copy Markdown
Member

Just need to test that we don't break developer venv or dev containers

@agners

agners commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

I've run the tests in independently again, and I can reproduce the CPU/memory benefits. From my perspective the main advantage using mimalloc over jemalloc is it's backing by upstream Python: It is the default for free-threaded builds, and should therefor be well supported. The fact that it support multiple page sizes at runtime makes it also the better option when it comes to architecture compatibility.

@agners
agners marked this pull request as ready for review July 7, 2026 07:35
@agners

agners commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Also note that the peak RSS size is probably mostly an artifact of the jemalloc tunning via MALLOC_CONF: We set decay_ms:20000, which delays de-allocation. Likely most of the memory is going to be free'd ultimately, just a bit delayed.

@agners

agners commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@bdraco do you have any concerns still with this change?

@agners
agners merged commit 3b12041 into home-assistant:dev Jul 13, 2026
48 checks passed
@agners
agners deleted the mimalloc-python-allocator branch July 13, 2026 18:24
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

cla-signed code-quality small-pr PRs with less than 30 lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants