Skip to content

ENH: cache large memory allocations on systems with MADV_FREE - #8783

Closed
juliantaylor wants to merge 2 commits into
numpy:masterfrom
juliantaylor:large-cache
Closed

juliantaylor wants to merge 2 commits into
numpy:masterfrom
juliantaylor:large-cache

Conversation

@juliantaylor

@juliantaylor juliantaylor commented Mar 14, 2017 •

Copy link
Copy Markdown
Contributor
Large allocations on glibc systems need to be faulted into the process
before use each use. This can account for a significant fraction of the
runtime of frequent array allocation and deallocation.  This can be avoidded
by caching used memory blocks inside numpy for later reuse.  To not waste
the memory in the cache we mark the memory as reclaimable by other processes
(or numpy itself) with MADV_FREE. Should we try to use memory claimed by
other processes we will receive newly faulted memory instead just as if it
were freed normally.
MADV_FREE is supported on Linux >= 4.5 and BSD.

The cache is intentionally very simple queue storing the last large
deallocations in a queue. New allocations pick the first block of memory
that matches in size from the cache. It does not pick larger blocks,
while the memory can be reclaimed by other processes it is not available
for the page cache used to buffer IO.
On each deallocation the cache is aged and if no entries have been
removed for too long the oldest memory block is properly deallocated to
avoid that large unused blocks stay in the cache too long.
In most cases where it is relevant are fast allocation/deallocation
cycles that appear in operations with temporaries or indexing. A simple
small cache is sufficient for this.
It will cache at most half of the available physical memory so that
there is always enough free to be used in the page cache.

still a prototype missing documentation and cleanup, but should be usable to test how effective it is in practice.

@njsmith

njsmith commented Mar 14, 2017

Copy link
Copy Markdown
Member

So if I understand correctly, if we do (1) allocate a large array, (2) free the large array, (3) allocate a small array, (4) free the small array, then we end up with a pointer in the cache that points to a large amount of address space, but we've "lost track" of most of that and think it's a pointer to a small amount of address space, right? Are we worried about the impact of this kind of address space leakage on 32-bit systems?

...also this is vague, but I wonder if we should be worried at all about transparent hugepages screwiness?

@juliantaylor

Copy link
Copy Markdown
Contributor Author

Indeed on 32 bit it should be disabled as this does waste address space.
Transparent hugepages should have no influence on this. As far as the kernel is concerned we are freeing memory normally so the fragmentation pattern should not change.

@charris

charris commented Mar 14, 2017

Copy link
Copy Markdown
Member

I'm going to schedule this for 1.14. I'd like to branch 1.13 soon and it is already loaded with risk.

@charris charris added this to the 1.14.0 release milestone Mar 14, 2017
@mattharrigan

Copy link
Copy Markdown
Contributor

A naive question, can these functions be called by multiple threads? Is thread safety a concern? In general I have a hard time understanding what can or cannot be safely called by multiple threads, and the GIL is another layer of complexity/protection.

@juliantaylor

juliantaylor commented Mar 16, 2017 •

Copy link
Copy Markdown
Contributor Author

Currently these functions must be called under a lock, namely the GIL.
But updating them to be threadsafe is very easy, just not necessary yet.

@homu

homu commented Apr 7, 2017

Copy link
Copy Markdown
Contributor

☔ The latest upstream changes (presumably #8907) made this pull request unmergeable. Please resolve the merge conflicts.

replace path seperators with underscore
@charris

charris commented Jun 2, 2017 •

Copy link
Copy Markdown
Member
     alloc.c
    numpy\core\src\multiarray\alloc.c(27) : fatal error C1083: Cannot open include file: 'unistd.h': No such file or directory

The unistd.h file does not exist in windows.

Comment thread numpy/core/src/multiarray/alloc.c Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

File does not exist on windows.

Large allocations on glibc systems need to be faulted into the process
before use each use. This can account for a significant fraction of the
runtime of frequent array allocation and deallocation.  This can be avoidded
by caching used memory blocks inside numpy for later reuse.  To not waste
the memory in the cache we mark the memory as reclaimable by other processes
(or numpy itself) with MADV_FREE. Should we try to use memory claimed by
other processes we will receive newly faulted memory instead just as if it
were freed normally.
MADV_FREE is supported on Linux >= 4.5 and BSD.

The cache is intentionally very simple queue storing the last large
deallocations in a queue. New allocations pick the first block of memory
that matches in size from the cache. It does not pick larger blocks,
while the memory can be reclaimed by other processes it is not available
for the page cache used to buffer IO.
On each deallocation the cache is aged and if no entries have been
removed for too long the oldest memory block is properly deallocated to
avoid that large unused blocks stay in the cache too long.
In most cases where it is relevant are fast allocation/deallocation
cycles that appear in operations with temporaries or indexing. A simple
small cache is sufficient for this.
It will cache at most half of the available physical memory so that
there is always enough free to be used in the page cache.
@juliantaylor

Copy link
Copy Markdown
Contributor Author

The code is still quite messy and still needs some changing (e.g. the exposed function is just for testing for now) but I think this approach should work.

I have changed it to address that the uncommitted memory is available for other processes but not for the page cache.
Instead of a store-largest-block cache it is now a simple queue limited to at most 50% of the physical memory, retrieving blocks from it requires an exact match so we do not leak out too large blocks to persistent allocations by the user. This makes the cache significantly less effective but it should still serve its purpose of avoiding page faults for the common same size alloc/dealloc cycle for temporaries.
Using only half of the RAM should not degrade OS IO caching significantly as applications using that much data probably need a large amount of allocated memory anyway.
To avoid that stuff hangs around in the cache for too long the oldest entry is cleared every 2000 memory allocations (all of them, not only big ones) unless the cache is actively used.

@charris charris modified the milestones: 1.14.0 release, 1.15.0 release Oct 17, 2017
@charris

charris commented Oct 17, 2017

Copy link
Copy Markdown
Member

@juliantaylor I'm going to push this off to 1.15 as it sounds like it needs more work and time,

@juliantaylor

Copy link
Copy Markdown
Contributor Author

well it needs review, the code cleanup is minor and can be done when agreed this concept works for all.

#else
#define PyTraceMalloc_Track(...)
#define PyTraceMalloc_Track(...) -2
#define PyTraceMalloc_Untrack(...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be -2 as well.

@charris

charris commented May 11, 2018

Copy link
Copy Markdown
Member

@juliantaylor Needs rebase.

@charris

charris commented May 16, 2018

Copy link
Copy Markdown
Member

This seems to be languishing. I want to do 1.15 soon, so going to push this off to 1.16.

@juliantaylor

Copy link
Copy Markdown
Contributor Author

Delaying it is fine but some opinions on whether we want this in or not would be useful. Maybe I should post it to the list?

@mattip

mattip commented May 17, 2018

Copy link
Copy Markdown
Member

Is this code/approach freshly developed for NumPy or has it been battle tested elsewhere? It seems like we need some kind of stress tests to make sure the cache works as advertised. I don't know enough about kernel internals to have an opinion on the impact this code might have on security, stability, and performance.

Maybe there could be a switch or environment variable to control cache use, so a single binary could be run in various modes, and we could default to having it off for a release or two, then turn it on?

@charris

charris commented Nov 13, 2018

Copy link
Copy Markdown
Member

I hate to keep pushing this off, but am going to do it again. @juliantaylor Needs a rebase. I think it would also help if it could be demonstrated to improve performance in common cases. As to threading safety, is Python threading (GIL) the only thing we need worry about? The libraries we use certainly do threading. There is currently other caching as well in Python itself, so I wonder if this will play well with that.

@charris charris modified the milestones: 1.16.0 release, 1.17.0 release Nov 13, 2018
@charris charris removed this from the 1.17.0 release milestone Jun 14, 2019
* Large memory allocation cache.
* Large allocations on glibc systems need to be faulted into the process
* before use each use. This can account for a significant fraction of the
* runtime of frequent array allocation and deallocation. This can be avoidded

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.

Suggested change
* runtime of frequent array allocation and deallocation. This can be avoidded
* runtime of frequent array allocation and deallocation. This can be avoided

@charris

charris commented Dec 17, 2020

Copy link
Copy Markdown
Member

I'm going to close this. @juliantaylor If you want to continue this line of thought please open a new PR.

@charris charris closed this Dec 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants