perf(docker): prove on jemalloc instead of glibc malloc - #432
Conversation
The prover allocates its scratch through the system allocator and frees it after each proof. glibc takes it back but does not return it to the OS: most lands in the main heap, which can only shrink from the top, so one live allocation above a freed block pins everything below it for the life of the process. jemalloc allocates only through mmap and releases pages on a decay timer. Measured on devnet with one node on jemalloc and three on glibc -- same image, same chain, same slot, same host, the allocator the only difference: node allocator RSS at slot 3,440 brk heap gean_0 jemalloc 448 MB 0 gean_1 glibc 1,150 MB 264 gean_2 glibc 1,135 MB 302 gean_3 glibc 1,126 MB 694 A 60% reduction, sustained for roughly 1,900 slots rather than an early-run artifact. The shape differs as much as the level: the jemalloc node oscillates 351-715 MB as memory is taken and returned, while the glibc nodes sit pinned flat because theirs cannot come back. Its peak stayed below their floor. No cost measured. The jemalloc node had the highest attestation signing rate of the four, the fastest aggregated-signature verification, the lowest CPU, an identical 0.800s tick, and stayed in sync with the same justified and finalized slots. Zero restarts. This supersedes the MALLOC_ARENA_MAX=2 approach, which was withdrawn from #427 before merge: capping arena count bounds retention but cannot return what the main heap already holds, it costs allocator concurrency that worsens as the validator set grows, and it was implicated in #430 -- a thread wedged while holding one of only two arenas took a whole node down. The soname is unqualified on purpose: ld.so resolves it per architecture and this image builds for arm64 as well. The ldconfig check fails the build rather than letting a missing library degrade into a silent runtime warning. Refs #424
a76c248 to
86f26cd
Compare
Final reading, slot 5,705The run continued 2,265 slots past the figures in the description. The result strengthens, and one nuance is worth recording.
65-69% lower floor, using Peaks are not lower, and that matters for how this is described. gean_0 peaked at 4,749 MB and gean_2 on glibc peaked at 4,841 MB — transient peaks are comparable between allocators. jemalloc does not constrain how much the prover takes; it changes whether the memory comes back afterwards. The floor is the figure that determines how many nodes fit on a host over hours, so "floor 341 MB vs 960-1,096 MB, peaks comparable" is the accurate description rather than a single headline number. Still no cost at this slot:
Identical signing rate, tied-fastest aggregate verification, lowest CPU, identical tick. Zero restarts across all four for the life of the run; justified and finalized tracked the other nodes throughout. |
Dockerfile path verified end-to-endThe description flagged that the mechanism had only been validated via An image built from The unqualified soname resolves and the preload loads — This run also puts jemalloc under the aggregator role for the first time — the earlier test had gean not aggregating. That is the configuration where the prover allocates hardest, and it is directly comparable to the original baseline (gean aggregating on glibc, RSS floors 2,691-3,429 MB), so the matched-slot comparison will be like-for-like on role as well as slot. Results to follow once the chain has depth. |
One Dockerfile change: install
libjemalloc2andLD_PRELOADit, so the prover's allocations go through jemalloc rather than glibc malloc.Refs #424, and supersedes the
MALLOC_ARENA_MAX=2approach that was withdrawn from #427 before merge.Why
The prover allocates its scratch through the system allocator and frees it after each proof. glibc takes it back but does not hand it to the OS. Most of it lands in the main heap, which can only shrink from the top — one live allocation above a freed block pins everything below it for the life of the process. jemalloc allocates only via
mmapand releases pages on a decay timer.Measurement
One node on jemalloc, three on glibc. Same image, same chain, same slot, same host — the allocator is the only difference.
60% reduction, sustained ~1,900 slots after the swap — not an early-run artifact.
The shape is the stronger evidence. The jemalloc node oscillates as memory is taken and returned; the glibc nodes are pinned flat because theirs cannot come back:
gean_0's peak (715 MB) stays below every control's floor (973 MB).
No measured cost
Highest signing rate, fastest aggregated-signature verification, lowest CPU, identical tick. Same justified and finalized slots as the other three, in sync, zero restarts.
Why not
MALLOC_ARENA_MAX=2It worked (~69% on its own measurement) but:
brkwas still 264–694 MB on the capped nodesvm_mmap_pgoffwhile holding one of only two arenas took a whole node down ~2.5 hours later, unkillable, requiring a host rebootjemalloc addresses the class of problem instead of capping a symptom.
Notes on the change
Unqualified soname is deliberate.
ld.soresolveslibjemalloc.so.2per architecture, and this image builds for arm64 too; a hardcodedx86_64path would silently degrade there. Verified thatldconfig -presolves the bare soname, and thatlibjemalloc2is the providing package.The
ldconfigcheck fails the build rather than letting a missing library become a runtime warning that's easy to miss — a silent failure here would look exactly like "jemalloc didn't help".Validation status — please read
The mechanism is validated on devnet, but via
LD_PRELOADwith the host's library bind-mounted into the running container, not via this Dockerfile. The library and the mechanism are identical; what has not been exercised end-to-end is the image build path.So on the first devnet run with an image built from this branch, confirm it actually took:
If the first is 0, the preload didn't load and the memory result will not reproduce.