Repository navigation
Conversation
| # If the system supports direct system memory access (hardware or | ||
| # software) we default to the system allocator. | ||
| if runtime.deviceGetAttribute( | ||
| runtime.cudaDevAttrPageableMemoryAccess, |
There was a problem hiding this comment.
One quick drive-by question: This does not differentiate between ATS (both cudaDevAttrPageableMemoryAccess and cudaDevAttrPageableMemoryAccessUsesHostPageTables are 1) and HMM (only the former is 1). Is it the intent?
I suppose it's safe since one can always explicitly overwrite.
There was a problem hiding this comment.
Hmmm, I got HMM not ATS (cudaDevAttrPageableMemoryAccessUsesHostPageTables is false) and things work fine.
But the question here is about defaults and after a (possibly too brief) search around it seems like whatever the system capabilities managed may well be a reasonable default if we assume narrow use with immediate GPU interaction (but it becomes less of an issue with ATS).
So maybe the default should just remain managed or we even just force passing it?
(I am not quite sure about the device check but if it's about default it gets complicated, I guess... Since one could modify the device within this context.)
There was a problem hiding this comment.
yeah both HMM and ATS would work, and I agree it seems better to set default to managed
| # software) we default to the system allocator. | ||
| if runtime.deviceGetAttribute( | ||
| runtime.cudaDevAttrPageableMemoryAccess, | ||
| device.get_device_id(), |
There was a problem hiding this comment.
Another question: Since ATS/HMM support is device-dependent, I am wondering if we should forbid the heterogeneous envs from turning on the special allocator. Seems safer to me to be conservative?
| elif arr.data.mem.size == 0: | ||
| return |
There was a problem hiding this comment.
Q: Do we want to special-case for 0-size arrays?
| if allocator == "managed": | ||
| return True |
There was a problem hiding this comment.
ditto, if we don't ban heterogeneous systems this technically isn't always True
| # SystemMemory / ManagedMemory and have no such attribute. | ||
| mem = self.data.mem | ||
| if isinstance(mem, (memory.SystemMemory, memory.ManagedMemory)): | ||
| return True |
| **Managed memory:** The above is also possible on systems without HMM support. In this | ||
| case ``CuPyNumPyAllocator()`` defaults to ``CuPyNumPyAllocator("managed")``, backing the | ||
| NumPy allocations with managed rather than system memory (otherwise the default is | ||
| ``CuPyNumPyAllocator("system")``). |
| You may also set the environment variable ``CUPY_ENABLE_UMP=1`` to relax checks further. | ||
| CuPy will accept NumPy arrays not strictly backed by its own allocator as well as support | ||
| system memory allocations in ``UnownedMemory``. |
There was a problem hiding this comment.
I don't follow how CUPY_ENABLE_UMP becomes optional in this PR?
There was a problem hiding this comment.
Well, optional in the sense that with this PR the following is a no-copy:
with CuPyNumPyAllocator():
a = np.arange(100)
cupy_a = cp.asarray(a)
On the other hand, CUPY_ENABLE_UMP=1 means you don't have to set a NumPy allocator at all.
(This is also assuming that there is no perf regression associated with checking for the allocator use.)
Which also answers the CI question: We may not be able to run CUPY_ENABLE_UMP=1 in a CI run, but we can install the managed memory allocator for NumPy on the global run (although it may slow things down).
leofang
left a comment
There was a problem hiding this comment.
A few additional comments inline.
Separately, nothing exercises the managed allocator path, ManagedMemory.from_external, is_device_accessible, the 0-size peer-access skip, or the reworked is_host_accessible. Given the subtlety here, some smoke coverage before merge would help.
-- Leo's bot
|
|
||
| # TODO: Should managed memory also use hint_hugepages? | ||
| # (Although if the case, that should be in memory.pyx!) | ||
| _managed_blocks[mem.ptr] = mem |
There was a problem hiding this comment.
Only _realloc_managed is marked with gil, with the note that NumPy sometimes calls realloc without the GIL. But _malloc_managed / _calloc_managed / _free_managed also touch Python objects (this dict, memory.malloc_managed, memory.MemoryPointer) and are only noexcept, so Cython assumes the GIL is held. If NumPy ever calls any of them without the GIL for the same reason it does with realloc, it's UB. Either mark all four with gil, or add a comment explaining why realloc is uniquely GIL-less.
| new_ptr = _malloc_managed(ctx, size) | ||
| if new_ptr == NULL: | ||
| return NULL | ||
| memcpy(new_ptr, ptr, old_mem.mem.size) |
There was a problem hiding this comment.
Same class of stream-order issue you flag in the PR body for no-copy NumPy dealloc: this host-side memcpy on managed memory races with any in-flight kernel still using the old buffer. Probably needs a runtime.streamSynchronize(...) before the copy.
There was a problem hiding this comment.
Yeah, this, the memory pool, and new tests are all still needed here. We also may want to bike-shed the name of the allocator once (maybe just cupy...numpy_allocator() even) since we probably want to make it public (and I had not done that yet).
| self._c_contiguous, self._f_contiguous, | ||
| (self.base is None | ||
| # TODO(seberg): MemoryPointer should have a flag for "owning"? | ||
| and getattr(self.data.mem, '_owner', None) is None)) |
There was a problem hiding this comment.
This handles the new managed/system wraps, but the pre-existing UnownedMemory has no _owner field either, so owndata still reports True for it. Your own TODO above notes MemoryPointer needs a real "owning" flag — worth doing here rather than leaving another latent case?
| cpython.PyErr_WriteUnraisable("CuPy malloc_managed") | ||
| return NULL | ||
|
|
||
| # TODO: Should managed memory also use hint_hugepages? |
There was a problem hiding this comment.
Two items stacked here that read as blockers rather than TODOs to me:
memory.malloc_managed(size)bypasses the CuPy memory pool — every NumPy allocation hitscudaMallocManageddirectly (also called out in the PR body).- No hugepage hint on managed allocations.
Fine to defer if there's a plan for a follow-up, but worth naming as such.
| if allocator == "managed": | ||
| return True | ||
| if allocator == "system": | ||
| return runtime.deviceGetAttribute( |
There was a problem hiding this comment.
Every cp.asarray(np_arr) on the system path pays a runtime API call here. Worth caching per device (or hoisting into is_ump_supported alongside _is_ump_enabled).
| cpython.PyObject* PyArray_HANDLER(cnp.ndarray arr) | ||
|
|
||
|
|
||
| # Similar to, but simplfied from, the NumPy alloc.cpp code |
|
|
||
| # Define an allocator for aligned memory (only useful with UMP supported) | ||
| cdef cnp.PyDataMem_Handler _aligned_handler | ||
| # The init.pyd file uses `char *` rather than `char[127]`, making awkward: |
There was a problem hiding this comment.
nit: .pyd → .pxd (same typo on L202)
| cdef readonly str kind | ||
|
|
||
| def __cinit__(self, kind=None): | ||
| """Initialize the NumPy allcator. |
| cpdef inline _check_peer_access(_ndarray_base arr, int device_id): | ||
| if arr.data.device_id == device_id: | ||
| return | ||
| elif arr.data.mem.size == 0: |
There was a problem hiding this comment.
nit: elif after an unconditional return — plain if reads cleaner.
|
Another thing, non-blocking: We probably should figure out a way to test this in the CI, at least nightly... |
|
This pull request is now in conflicts. Could you fix it @seberg? 🙏 |
This is towards #8711 although as discussed earlier: it is already possible to "proper" UMP on normal hardware with new linux kernels, it generally "just works".
@leofang if you have some time, this does:
Locally the test seemed passing (but not with
-n16hinting to potential stream order issues), but since I suspect there may be a few things to figure out I'll keep this draft. For one thing, there are some issues around stream-order and others (malloc_managed()might not be using the pool, theowndatalogic isn't nice, and maybe most importantly,streamorder of a no-copy NumPy arrays deallocation is probably wrong -- we probably need to delay the full deallocate there).