Repository navigation
samples: demonstrate Grace Hopper host and managed memory #3278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
0z5a
wants to merge
3
commits into
NVIDIA:main
Choose a base branch
from
0z5a:samples/grace-hopper-memory
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Sample: Grace Hopper Memory | ||
|
|
||
| On Grace Hopper, NVLink-C2C makes CPU memory coherently accessible to the GPU, | ||
| and the GPU translates addresses through the host page tables. GPU code can | ||
| therefore work in place on pinned, managed and ordinary pageable host memory, | ||
| and the CPU sees the results without a copy. | ||
|
|
||
| This sample allocates one buffer from each `cuda.core` memory resource, plus a | ||
| plain NumPy array: | ||
|
|
||
| - `PinnedMemoryResource` on the GPU's nearest host NUMA node | ||
| - `ManagedMemoryResource` | ||
| - pageable memory from `np.empty()` | ||
| - `DeviceMemoryResource`, for comparison | ||
|
|
||
| It wraps each buffer as a CuPy array and uses standard CuPy operations, with no | ||
| custom kernels. For each host-accessible buffer the CPU writes the input, CuPy | ||
| doubles it and sums it on the GPU, and the CPU checks the result in the same | ||
| memory. Device memory needs an explicit copy in each direction. | ||
|
|
||
| `cp.from_dlpack()` imports the device and managed buffers. It does not import | ||
| pinned (`kDLCUDAHost`) or CPU (`kDLCPU`) memory, so the sample wraps those | ||
| pointers with `cupy.cuda.UnownedMemory`. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - A Grace Hopper system: the GPU must access pageable memory through the host | ||
| page tables, support concurrent managed access and support host NUMA memory | ||
| pools. Other systems waive with exit code 2 (configurable via | ||
| `CUDA_PYTHON_SAMPLE_WAIVER_EXIT_CODE`). | ||
| - `cuda-core` 1.2.2 or newer with CUDA 13, CuPy for CUDA 13 and NumPy 2.2.5 | ||
| or newer. | ||
|
|
||
| ```bash | ||
| cd samples/cuda_core/graceHopperMemory | ||
| pip install -r requirements.txt | ||
| python graceHopperMemory.py | ||
| ``` | ||
|
|
||
| ## Example Output | ||
|
|
||
| On a GH200 480GB: | ||
|
|
||
| ```text | ||
| Device: NVIDIA GH200 480GB, compute capability 9.0 | ||
| host_numa_id: 0 | ||
| pageable_memory_access_uses_host_page_tables: True | ||
| concurrent_managed_access: True | ||
| host_numa_memory_pools_supported: True | ||
| GPU work on host-resident memory: | ||
| pinned, host NUMA node 0 sum=17163091968 verified on the CPU without a copy | ||
| managed sum=17163091968 verified on the CPU without a copy | ||
| pageable NumPy array sum=17163091968 verified on the CPU without a copy | ||
| For comparison, device memory needs explicit copies: | ||
| device verified after a device-to-host copy | ||
| Done | ||
| ``` |
154 changes: 154 additions & 0 deletions
154
samples/cuda_core/graceHopperMemory/graceHopperMemory.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Redistribution and use in source and binary forms, with or without | ||
| # modification, are permitted provided that the following conditions | ||
| # are met: | ||
| # * Redistributions of source code must retain the above copyright | ||
| # notice, this list of conditions and the following disclaimer. | ||
| # * Redistributions in binary form must reproduce the above copyright | ||
| # notice, this list of conditions and the following disclaimer in the | ||
| # documentation and/or other materials provided with the distribution. | ||
| # * Neither the name of NVIDIA CORPORATION nor the names of its | ||
| # contributors may be used to endorse or promote products derived | ||
| # from this software without specific prior written permission. | ||
| # | ||
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY | ||
| # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | ||
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | ||
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | ||
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | ||
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | ||
| # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
|
|
||
| # /// script | ||
| # dependencies = ["cuda-core[cu13]>=1.2.2", "cupy-cuda13x>=14.0.0", "numpy>=2.2.5"] | ||
| # /// | ||
|
|
||
| """Run CuPy directly on host memory on Grace Hopper. | ||
|
|
||
| On Grace Hopper, NVLink-C2C makes CPU memory coherently accessible to the GPU, | ||
| and the GPU translates addresses through the host page tables. A kernel can | ||
| therefore read and write pinned, managed and even ordinary pageable host memory | ||
| in place, and the CPU sees the results without a copy. | ||
|
|
||
| The sample allocates one buffer from each cuda.core memory resource (device, | ||
| pinned on the GPU's nearest host NUMA node, and managed) plus a plain NumPy | ||
| array. It wraps each one as a CuPy array, runs the same CuPy operations on all | ||
| of them and checks the results on the CPU. | ||
| """ | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
| try: | ||
| import cupy as cp | ||
| import numpy as np | ||
|
|
||
| from cuda.core import ( | ||
| Device, | ||
| DeviceMemoryResource, | ||
| ManagedMemoryResource, | ||
| PinnedMemoryResource, | ||
| PinnedMemoryResourceOptions, | ||
| ) | ||
| except ImportError as e: | ||
| print(f"Error: Required package not found: {e}") | ||
| print("Install with: pip install -r requirements.txt") | ||
| sys.exit(1) | ||
|
|
||
| EXIT_WAIVED = int(os.environ.get("CUDA_PYTHON_SAMPLE_WAIVER_EXIT_CODE", "2")) | ||
| N = 1 << 24 # 64 MiB of float32 values per buffer | ||
|
|
||
|
|
||
| def host_as_cupy(ptr, nbytes, owner, device_id): | ||
| # cp.from_dlpack() does not import host memory (kDLCUDAHost or kDLCPU), so | ||
| # wrap the pointer directly. owner keeps the memory alive. | ||
| memory = cp.cuda.UnownedMemory(ptr, nbytes, owner, device_id=device_id) | ||
| return cp.ndarray((nbytes // 4,), cp.float32, cp.cuda.MemoryPointer(memory, 0)) | ||
|
|
||
|
|
||
| def run(label, gpu_array, cpu_array): | ||
| """Fill on the CPU, compute on the GPU, check on the CPU.""" | ||
| cpu_array[:] = np.arange(N, dtype=np.float32) % 1024 | ||
| gpu_array *= 2 | ||
| total = int(gpu_array.sum(dtype=cp.float64)) | ||
| cp.cuda.get_current_stream().synchronize() | ||
|
|
||
| expected = 2 * (np.arange(N, dtype=np.float32) % 1024) | ||
| if total != int(expected.sum(dtype=np.float64)) or not np.array_equal(cpu_array, expected): | ||
| raise RuntimeError(f"{label}: result mismatch") | ||
| print(f" {label:<38} sum={total} verified on the CPU without a copy") | ||
|
|
||
|
|
||
| def main(): | ||
| dev = Device(0) | ||
| dev.set_current() | ||
| props = dev.properties | ||
| cc = dev.compute_capability | ||
| print(f"Device: {dev.name}, compute capability {cc.major}.{cc.minor}") | ||
| print(f" host_numa_id: {props.host_numa_id}") | ||
| print(f" pageable_memory_access_uses_host_page_tables: {props.pageable_memory_access_uses_host_page_tables}") | ||
| print(f" concurrent_managed_access: {props.concurrent_managed_access}") | ||
| print(f" host_numa_memory_pools_supported: {props.host_numa_memory_pools_supported}") | ||
|
|
||
| if not ( | ||
| props.pageable_memory_access_uses_host_page_tables | ||
| and props.concurrent_managed_access | ||
| and props.host_numa_memory_pools_supported | ||
| and props.host_numa_id >= 0 | ||
| ): | ||
| print("This sample requires a Grace Hopper class system; waiving.") | ||
| return EXIT_WAIVED | ||
|
|
||
| stream = dev.create_stream() | ||
| nbytes = N * 4 | ||
| device_buffer = DeviceMemoryResource(dev).allocate(nbytes, stream=stream) | ||
| pinned_buffer = PinnedMemoryResource(PinnedMemoryResourceOptions(numa_id=props.host_numa_id)).allocate( | ||
| nbytes, stream=stream | ||
| ) | ||
| managed_buffer = ManagedMemoryResource().allocate(nbytes, stream=stream) | ||
| pageable = np.empty(N, dtype=np.float32) | ||
| # Allocations are ordered on stream; CuPy uses its own stream. | ||
| stream.sync() | ||
|
|
||
| try: | ||
| print("GPU work on host-resident memory:") | ||
| run( | ||
| f"pinned, host NUMA node {props.host_numa_id}", | ||
| host_as_cupy(pinned_buffer.handle, nbytes, pinned_buffer, dev.device_id), | ||
| np.from_dlpack(pinned_buffer).view(np.float32), | ||
| ) | ||
| run( | ||
| "managed", | ||
| cp.from_dlpack(managed_buffer).view(cp.float32), | ||
| np.from_dlpack(managed_buffer).view(np.float32), | ||
| ) | ||
| run( | ||
| "pageable NumPy array", | ||
| host_as_cupy(pageable.ctypes.data, nbytes, pageable, dev.device_id), | ||
| pageable, | ||
| ) | ||
|
|
||
| print("For comparison, device memory needs explicit copies:") | ||
| device_array = cp.from_dlpack(device_buffer).view(cp.float32) | ||
| device_array.set(np.arange(N, dtype=np.float32) % 1024) | ||
| device_array *= 2 | ||
| if not np.array_equal(device_array.get(), 2 * (np.arange(N, dtype=np.float32) % 1024)): | ||
| raise RuntimeError("device: result mismatch") | ||
| print(f" {'device':<38} verified after a device-to-host copy") | ||
| finally: | ||
| cp.cuda.get_current_stream().synchronize() | ||
| for buffer in (managed_buffer, pinned_buffer, device_buffer): | ||
| buffer.close(stream) | ||
| stream.sync() | ||
|
|
||
| print("Done") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| cuda-core[cu13]>=1.2.2 | ||
| cupy-cuda13x>=14.0.0 | ||
| numpy>=2.2.5 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example seems overly complicated here. What are we trying to achieve?
We can allocate buffers from different MRs, and then wrap them as CuPy arrays via
cp.from_dlpack(buf). Then, all the custom kernels can be replaced by standard CuPy ops.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I removed the custom kernels, NVRTC and the bandwidth timing. The goal is to show that on Grace Hopper the GPU can work in place on host memory and the CPU sees the result without a copy. The sample now allocates from
PinnedMemoryResource(on the GPU's host NUMA node),ManagedMemoryResourceandDeviceMemoryResource, adds a plain NumPy array as pageable memory, wraps each one as a CuPy array, and uses only CuPy ops (*=,sum). The CPU writes the input and checks the result in the same memory.One snag:
cp.from_dlpack()works for the device and managed buffers, but CuPy 14.2 rejects the pinned buffer (kDLCUDAHost:TypeError: Unsupported array type: 3) and NumPy memory (kDLCPU). For those two the sample wraps the pointer withcupy.cuda.UnownedMemoryin a small helper. Is there a better way to do this in CuPy?Verified on GH200.