Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions samples/cuda_core/graceHopperMemory/README.md
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 samples/cuda_core/graceHopperMemory/graceHopperMemory.py

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.

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.

Copy link
Copy Markdown
Contributor Author

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), ManagedMemoryResource and DeviceMemoryResource, 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 with cupy.cuda.UnownedMemory in a small helper. Is there a better way to do this in CuPy?

Verified on GH200.

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())
3 changes: 3 additions & 0 deletions samples/cuda_core/graceHopperMemory/requirements.txt
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
Loading