SWDEV-514685 - Update documentation 2025-02-11 (#25)

Co-authored-by: Istvan Kiss <neon60@gmail.com>
Dieser Commit ist enthalten in:
Kiss, Istvan
2025-03-19 22:04:47 +01:00
committet von GitHub
Ursprung 5eeebdbf4f
Commit 69becfc7a8
41 geänderte Dateien mit 3318 neuen und 1879 gelöschten Zeilen
@@ -0,0 +1,534 @@
.. meta::
:description: This topic describes asynchronous concurrent execution in HIP
:keywords: AMD, ROCm, HIP, asynchronous concurrent execution, asynchronous, async, concurrent, concurrency
.. _asynchronous_how-to:
*******************************************************************************
Asynchronous concurrent execution
*******************************************************************************
Asynchronous concurrent execution is important for efficient parallelism and
resource utilization, with techniques such as overlapping computation and data
transfer, managing concurrent kernel execution with streams on single or
multiple devices, or using HIP graphs.
Streams and concurrent execution
===============================================================================
All asynchronous APIs, such as kernel execution, data movement and potentially
data allocation/freeing all happen in the context of device streams.
Streams are FIFO buffers of commands to execute in order on a given device.
Commands which enqueue tasks on a stream all return promptly and the task is
executed asynchronously. Multiple streams can point to the same device and
those streams might be fed from multiple concurrent host-side threads. Multiple
streams tied to the same device are not guaranteed to execute their commands in
order.
Managing streams
-------------------------------------------------------------------------------
Streams enable the overlap of computation and data transfer, ensuring
continuous GPU activity. By enabling tasks to run concurrently within the same
GPU or across different GPUs, streams improve performance and throughput in
high-performance computing (HPC).
To create a stream, the following functions are used, each defining a handle
to the newly created stream:
- :cpp:func:`hipStreamCreate`: Creates a stream with default settings.
- :cpp:func:`hipStreamCreateWithFlags`: Creates a stream, with specific
flags, listed below, enabling more control over stream behavior:
- ``hipStreamDefault``: creates a default stream suitable for most
operations. The default stream is a blocking operation.
- ``hipStreamNonBlocking``: creates a non-blocking stream, allowing
concurrent execution of operations. It ensures that tasks can run
simultaneously without waiting for each other to complete, thus improving
overall performance.
- :cpp:func:`hipStreamCreateWithPriority`: Allows creating a stream with a
specified priority, enabling prioritization of certain tasks.
The :cpp:func:`hipStreamSynchronize` function is used to block the calling host
thread until all previously submitted tasks in a specified HIP stream have
completed. It ensures that all operations in the given stream, such as kernel
executions or memory transfers, are finished before the host thread proceeds.
.. note::
If the :cpp:func:`hipStreamSynchronize` function input stream is 0 (or the
default stream), it waits for all operations in the default stream to
complete.
Concurrent execution between host and device
-------------------------------------------------------------------------------
Concurrent execution between the host (CPU) and device (GPU) allows the CPU to
perform other tasks while the GPU is executing kernels. Kernels are launched
asynchronously using ``hipLaunchKernelGGL`` or using the triple chevron with a stream,
enabling the CPU to continue executing other code while the GPU processes the
kernel. Similarly, memory operations like :cpp:func:`hipMemcpyAsync` are
performed asynchronously, allowing data transfers between the host and device
without blocking the CPU.
Concurrent kernel execution
-------------------------------------------------------------------------------
Concurrent execution of multiple kernels on the GPU allows different kernels to
run simultaneously to maximize GPU resource usage. Managing dependencies
between kernels is crucial for ensuring correct execution order. This can be
achieved using :cpp:func:`hipStreamWaitEvent`, which allows a kernel to wait
for a specific event before starting execution.
Independent kernels can only run concurrently if there are enough registers
and shared memory for the kernels. To enable concurrent kernel executions, the
developer may have to reduce the block size of the kernels. The kernel runtimes
can be misleading for concurrent kernel runs, that is why during optimization
it is a good practice to check the trace files, to see if one kernel is blocking
another kernel, while they are running in parallel. For more information about
the application tracing, check::doc:`rocprofiler:/how-to/using-rocprof`.
When running kernels in parallel, the execution time can increase due to
contention for shared resources. This is because multiple kernels may attempt
to access the same GPU resources simultaneously, leading to delays.
Multiple kernels executing concurrently is only beneficial under specific conditions. It
is most effective when the kernels do not fully utilize the GPU's resources. In
such cases, overlapping kernel execution can improve overall throughput and
efficiency by keeping the GPU busy without exceeding its capacity.
Overlap of data transfer and kernel execution
===============================================================================
One of the primary benefits of asynchronous operations and multiple streams is
the ability to overlap data transfer with kernel execution, leading to better
resource utilization and improved performance.
Asynchronous execution is particularly advantageous in iterative processes. For
instance, if a kernel is initiated, it can be efficient to prepare the input
data simultaneously, provided that this preparation does not depend on the
kernel's execution. Such iterative data transfer and kernel execution overlap
can be find in the :ref:`async_example`.
Querying device capabilities
-------------------------------------------------------------------------------
Some AMD HIP-enabled devices can perform asynchronous memory copy operations to
or from the GPU concurrently with kernel execution. Applications can query this
capability by checking the ``asyncEngineCount`` device property. Devices with
an ``asyncEngineCount`` greater than zero support concurrent data transfers.
Additionally, if host memory is involved in the copy, it should be page-locked
to ensure optimal performance. Page-locking (or pinning) host memory increases
the bandwidth between the host and the device, reducing the overhead associated
with data transfers. For more details, visit :ref:`host_memory` page.
Asynchronous memory operations
-------------------------------------------------------------------------------
Asynchronous memory operations do not block the host while copying data and,
when used with multiple streams, allow data to be transferred between the host
and device while kernels are executed on the same GPU. Using operations like
:cpp:func:`hipMemcpyAsync` or :cpp:func:`hipMemcpyPeerAsync`, developers can
initiate data transfers without waiting for the previous operation to complete.
This overlap of computation and data transfer ensures that the GPU is not idle
while waiting for data. :cpp:func:`hipMemcpyPeerAsync` enables data transfers
between different GPUs, facilitating multi-GPU communication.
:ref:`async_example`` include launching kernels in one stream while performing
data transfers in another. This technique is especially useful in applications
with large data sets that need to be processed quickly.
Concurrent data transfers with intra-device copies
-------------------------------------------------------------------------------
Devices that support the ``concurrentKernels`` property can perform
intra-device copies concurrently with kernel execution. Additionally, devices
that support the ``asyncEngineCount`` property can perform data transfers to
or from the GPU simultaneously with kernel execution. Intra-device copies can
be initiated using standard memory copy functions with destination and source
addresses residing on the same device.
Synchronization, event management and synchronous calls
===============================================================================
Synchronization and event management are important for coordinating tasks and
ensuring correct execution order, and synchronous calls are necessary for
maintaining data consistency.
Synchronous calls
-------------------------------------------------------------------------------
Synchronous calls ensure task completion before moving to the next operation.
For example, :cpp:func:`hipMemcpy` for data transfers waits for completion
before returning control to the host. Similarly, synchronous kernel launches
are used when immediate completion is required. When a synchronous function is
called, control is not returned to the host thread before the device has
completed the requested task. The behavior of the host thread—whether to yield,
block, or spin—can be specified using :cpp:func:`hipSetDeviceFlags` with
appropriate flags. Understanding when to use synchronous calls is important for
managing execution flow and avoiding data races.
Events for synchronization
-------------------------------------------------------------------------------
By creating an event with :cpp:func:`hipEventCreate` and recording it with
:cpp:func:`hipEventRecord`, developers can synchronize operations across
streams, ensuring correct task execution order. :cpp:func:`hipEventSynchronize`
lets the application wait for an event to complete before proceeding with the next
operation.
Programmatic dependent launch and synchronization
-------------------------------------------------------------------------------
While CUDA supports programmatic dependent launches allowing a secondary kernel
to start before the primary kernel finishes, HIP achieves similar functionality
using streams and events. By employing :cpp:func:`hipStreamWaitEvent`, it is
possible to manage the execution order without explicit hardware support. This
mechanism allows a secondary kernel to launch as soon as the necessary
conditions are met, even if the primary kernel is still running.
.. _async_example:
Example
-------------------------------------------------------------------------------
The examples shows the difference between sequential, asynchronous calls and
asynchronous calls with ``hipEvents``.
.. figure:: ../../data/how-to/hip_runtime_api/asynchronous/sequential_async_event.svg
:alt: Compare the different calls
:align: center
The example codes
.. tab-set::
.. tab-item:: Sequential
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <vector>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t status = expression; \
if(status != hipSuccess){ \
std::cerr << "HIP error " \
<< status << ": " \
<< hipGetErrorString(status) \
<< " at " << __FILE__ << ":" \
<< __LINE__ << std::endl; \
} \
}
// GPU Kernels
__global__ void kernelA(double* arrayA, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] += 1.0;}
};
__global__ void kernelB(double* arrayA, double* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayB[x] += arrayA[x] + 3.0;}
};
int main()
{
constexpr int numOfBlocks = 1 << 20;
constexpr int threadsPerBlock = 1024;
constexpr int numberOfIterations = 50;
// The array size smaller to avoid the relatively short kernel launch compared to memory copies
constexpr size_t arraySize = 1U << 25;
double *d_dataA;
double *d_dataB;
double initValueA = 0.0;
double initValueB = 2.0;
std::vector<double> vectorA(arraySize, initValueA);
std::vector<double> vectorB(arraySize, initValueB);
// Allocate device memory
HIP_CHECK(hipMalloc(&d_dataA, arraySize * sizeof(*d_dataA)));
HIP_CHECK(hipMalloc(&d_dataB, arraySize * sizeof(*d_dataB)));
for(int iteration = 0; iteration < numberOfIterations; iteration++)
{
// Host to Device copies
HIP_CHECK(hipMemcpy(d_dataA, vectorA.data(), arraySize * sizeof(*d_dataA), hipMemcpyHostToDevice));
HIP_CHECK(hipMemcpy(d_dataB, vectorB.data(), arraySize * sizeof(*d_dataB), hipMemcpyHostToDevice));
// Launch the GPU kernels
hipLaunchKernelGGL(kernelA, dim3(numOfBlocks), dim3(threadsPerBlock), 0, 0, d_dataA, arraySize);
hipLaunchKernelGGL(kernelB, dim3(numOfBlocks), dim3(threadsPerBlock), 0, 0, d_dataA, d_dataB, arraySize);
// Device to Host copies
HIP_CHECK(hipMemcpy(vectorA.data(), d_dataA, arraySize * sizeof(*vectorA.data()), hipMemcpyDeviceToHost));
HIP_CHECK(hipMemcpy(vectorB.data(), d_dataB, arraySize * sizeof(*vectorB.data()), hipMemcpyDeviceToHost));
}
// Wait for all operations to complete
HIP_CHECK(hipDeviceSynchronize());
// Verify results
const double expectedA = (double)numberOfIterations;
const double expectedB =
initValueB + (3.0 * numberOfIterations) +
(expectedA * (expectedA + 1.0)) / 2.0;
bool passed = true;
for(size_t i = 0; i < arraySize; ++i){
if(vectorA[i] != expectedA){
passed = false;
std::cerr << "Validation failed! Expected " << expectedA << " got " << vectorA[i] << " at index: " << i << std::endl;
break;
}
if(vectorB[i] != expectedB){
passed = false;
std::cerr << "Validation failed! Expected " << expectedB << " got " << vectorB[i] << " at index: " << i << std::endl;
break;
}
}
if(passed){
std::cout << "Sequential execution completed successfully." << std::endl;
}else{
std::cerr << "Sequential execution failed." << std::endl;
}
// Cleanup
HIP_CHECK(hipFree(d_dataA));
HIP_CHECK(hipFree(d_dataB));
return 0;
}
.. tab-item:: Asynchronous
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <vector>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t status = expression; \
if(status != hipSuccess){ \
std::cerr << "HIP error " \
<< status << ": " \
<< hipGetErrorString(status) \
<< " at " << __FILE__ << ":" \
<< __LINE__ << std::endl; \
} \
}
// GPU Kernels
__global__ void kernelA(double* arrayA, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] += 1.0;}
};
__global__ void kernelB(double* arrayA, double* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayB[x] += arrayA[x] + 3.0;}
};
int main()
{
constexpr int numOfBlocks = 1 << 20;
constexpr int threadsPerBlock = 1024;
constexpr int numberOfIterations = 50;
// The array size smaller to avoid the relatively short kernel launch compared to memory copies
constexpr size_t arraySize = 1U << 25;
double *d_dataA;
double *d_dataB;
double initValueA = 0.0;
double initValueB = 2.0;
std::vector<double> vectorA(arraySize, initValueA);
std::vector<double> vectorB(arraySize, initValueB);
// Allocate device memory
HIP_CHECK(hipMalloc(&d_dataA, arraySize * sizeof(*d_dataA)));
HIP_CHECK(hipMalloc(&d_dataB, arraySize * sizeof(*d_dataB)));
// Create streams
hipStream_t streamA, streamB;
HIP_CHECK(hipStreamCreate(&streamA));
HIP_CHECK(hipStreamCreate(&streamB));
for(unsigned int iteration = 0; iteration < numberOfIterations; iteration++)
{
// Stream 1: Host to Device 1
HIP_CHECK(hipMemcpyAsync(d_dataA, vectorA.data(), arraySize * sizeof(*d_dataA), hipMemcpyHostToDevice, streamA));
// Stream 2: Host to Device 2
HIP_CHECK(hipMemcpyAsync(d_dataB, vectorB.data(), arraySize * sizeof(*d_dataB), hipMemcpyHostToDevice, streamB));
// Stream 1: Kernel 1
hipLaunchKernelGGL(kernelA, dim3(numOfBlocks), dim3(threadsPerBlock), 0, streamA, d_dataA, arraySize);
// Wait for streamA finish
HIP_CHECK(hipStreamSynchronize(streamA));
// Stream 2: Kernel 2
hipLaunchKernelGGL(kernelB, dim3(numOfBlocks), dim3(threadsPerBlock), 0, streamB, d_dataA, d_dataB, arraySize);
// Stream 1: Device to Host 2 (after Kernel 1)
HIP_CHECK(hipMemcpyAsync(vectorA.data(), d_dataA, arraySize * sizeof(*vectorA.data()), hipMemcpyDeviceToHost, streamA));
// Stream 2: Device to Host 2 (after Kernel 2)
HIP_CHECK(hipMemcpyAsync(vectorB.data(), d_dataB, arraySize * sizeof(*vectorB.data()), hipMemcpyDeviceToHost, streamB));
}
// Wait for all operations in both streams to complete
HIP_CHECK(hipStreamSynchronize(streamA));
HIP_CHECK(hipStreamSynchronize(streamB));
// Verify results
double expectedA = (double)numberOfIterations;
double expectedB =
initValueB + (3.0 * numberOfIterations) +
(expectedA * (expectedA + 1.0)) / 2.0;
bool passed = true;
for(size_t i = 0; i < arraySize; ++i){
if(vectorA[i] != expectedA){
passed = false;
std::cerr << "Validation failed! Expected " << expectedA << " got " << vectorA[i] << " at index: " << i << std::endl;
break;
}
if(vectorB[i] != expectedB){
passed = false;
std::cerr << "Validation failed! Expected " << expectedB << " got " << vectorB[i] << " at index: " << i << std::endl;
break;
}
}
if(passed){
std::cout << "Asynchronous execution completed successfully." << std::endl;
}else{
std::cerr << "Asynchronous execution failed." << std::endl;
}
// Cleanup
HIP_CHECK(hipStreamDestroy(streamA));
HIP_CHECK(hipStreamDestroy(streamB));
HIP_CHECK(hipFree(d_dataA));
HIP_CHECK(hipFree(d_dataB));
return 0;
}
.. tab-item:: hipStreamWaitEvent
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <vector>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t status = expression; \
if(status != hipSuccess){ \
std::cerr << "HIP error " \
<< status << ": " \
<< hipGetErrorString(status) \
<< " at " << __FILE__ << ":" \
<< __LINE__ << std::endl; \
} \
}
// GPU Kernels
__global__ void kernelA(double* arrayA, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] += 1.0;}
};
__global__ void kernelB(double* arrayA, double* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayB[x] += arrayA[x] + 3.0;}
};
int main()
{
constexpr int numOfBlocks = 1 << 20;
constexpr int threadsPerBlock = 1024;
constexpr int numberOfIterations = 50;
// The array size smaller to avoid the relatively short kernel launch compared to memory copies
constexpr size_t arraySize = 1U << 25;
double *d_dataA;
double *d_dataB;
double initValueA = 0.0;
double initValueB = 2.0;
std::vector<double> vectorA(arraySize, initValueA);
std::vector<double> vectorB(arraySize, initValueB);
// Allocate device memory
HIP_CHECK(hipMalloc(&d_dataA, arraySize * sizeof(*d_dataA)));
HIP_CHECK(hipMalloc(&d_dataB, arraySize * sizeof(*d_dataB)));
// Create streams
hipStream_t streamA, streamB;
HIP_CHECK(hipStreamCreate(&streamA));
HIP_CHECK(hipStreamCreate(&streamB));
// Create events
hipEvent_t event, eventA, eventB;
HIP_CHECK(hipEventCreate(&event));
HIP_CHECK(hipEventCreate(&eventA));
HIP_CHECK(hipEventCreate(&eventB));
for(unsigned int iteration = 0; iteration < numberOfIterations; iteration++)
{
// Stream 1: Host to Device 1
HIP_CHECK(hipMemcpyAsync(d_dataA, vectorA.data(), arraySize * sizeof(*d_dataA), hipMemcpyHostToDevice, streamA));
// Stream 2: Host to Device 2
HIP_CHECK(hipMemcpyAsync(d_dataB, vectorB.data(), arraySize * sizeof(*d_dataB), hipMemcpyHostToDevice, streamB));
// Stream 1: Kernel 1
hipLaunchKernelGGL(kernelA, dim3(numOfBlocks), dim3(threadsPerBlock), 0, streamA, d_dataA, arraySize);
// Record event after the GPU kernel in Stream 1
HIP_CHECK(hipEventRecord(event, streamA));
// Stream 2: Wait for event before starting Kernel 2
HIP_CHECK(hipStreamWaitEvent(streamB, event, 0));
// Stream 2: Kernel 2
hipLaunchKernelGGL(kernelB, dim3(numOfBlocks), dim3(threadsPerBlock), 0, streamB, d_dataA, d_dataB, arraySize);
// Stream 1: Device to Host 2 (after Kernel 1)
HIP_CHECK(hipMemcpyAsync(vectorA.data(), d_dataA, arraySize * sizeof(*vectorA.data()), hipMemcpyDeviceToHost, streamA));
// Stream 2: Device to Host 2 (after Kernel 2)
HIP_CHECK(hipMemcpyAsync(vectorB.data(), d_dataB, arraySize * sizeof(*vectorB.data()), hipMemcpyDeviceToHost, streamB));
// Wait for all operations in both streams to complete
HIP_CHECK(hipEventRecord(eventA, streamA));
HIP_CHECK(hipEventRecord(eventB, streamB));
HIP_CHECK(hipStreamWaitEvent(streamA, eventA, 0));
HIP_CHECK(hipStreamWaitEvent(streamB, eventB, 0));
}
// Verify results
double expectedA = (double)numberOfIterations;
double expectedB =
initValueB + (3.0 * numberOfIterations) +
(expectedA * (expectedA + 1.0)) / 2.0;
bool passed = true;
for(size_t i = 0; i < arraySize; ++i){
if(vectorA[i] != expectedA){
passed = false;
std::cerr << "Validation failed! Expected " << expectedA << " got " << vectorA[i] << std::endl;
break;
}
if(vectorB[i] != expectedB){
passed = false;
std::cerr << "Validation failed! Expected " << expectedB << " got " << vectorB[i] << std::endl;
break;
}
}
if(passed){
std::cout << "Asynchronous execution with events completed successfully." << std::endl;
}else{
std::cerr << "Asynchronous execution with events failed." << std::endl;
}
// Cleanup
HIP_CHECK(hipEventDestroy(event));
HIP_CHECK(hipEventDestroy(eventA));
HIP_CHECK(hipEventDestroy(eventB));
HIP_CHECK(hipStreamDestroy(streamA));
HIP_CHECK(hipStreamDestroy(streamB));
HIP_CHECK(hipFree(d_dataA));
HIP_CHECK(hipFree(d_dataB));
return 0;
}
HIP Graphs
===============================================================================
HIP graphs offer an efficient alternative to the standard method of launching
GPU tasks via streams. Comprising nodes for operations and edges for
dependencies, HIP graphs reduce kernel launch overhead and provide a high-level
abstraction for managing dependencies and synchronization. By representing
sequences of kernels and memory operations as a single graph, they simplify
complex workflows and enhance performance, particularly for applications with
intricate dependencies and multiple execution stages.
For more details, see the :ref:`how_to_HIP_graph` documentation.
@@ -1,52 +1,285 @@
.. meta::
:description: This chapter describes the device memory of the HIP ecosystem
ROCm software.
:keywords: AMD, ROCm, HIP, device memory
:keywords: AMD, ROCm, HIP, GPU, device memory, global, constant, texture, surface, shared
.. _device_memory:
*******************************************************************************
********************************************************************************
Device memory
*******************************************************************************
********************************************************************************
Device memory exists on the device, e.g. on GPUs in the video random access
memory (VRAM), and is accessible by the kernels operating on the device. Recent
architectures use graphics double data rate (GDDR) synchronous dynamic
random-access memory (SDRAM) such as GDDR6, or high-bandwidth memory (HBM) such
as HBM2e. Device memory can be allocated as global memory, constant, texture or
surface memory.
Device memory is random access memory that is physically located on a GPU. In
general it is memory with a bandwidth that is an order of magnitude higher
compared to RAM available to the host. That high bandwidth is only available to
on-device accesses, accesses from the host or other devices have to go over a
special interface which is considerably slower, usually the PCIe bus or the AMD
Infinity Fabric.
On certain architectures like APUs, the GPU and CPU share the same physical
memory.
There is also a special local data share on-chip directly accessible to the
:ref:`compute units <hardware_implementation>`, that can be used for shared
memory.
The physical device memory can be used to back up several different memory
spaces in HIP, as described in the following.
Global memory
================================================================================
Read-write storage visible to all threads on a given device. There are
specialized versions of global memory with different usage semantics which are
typically backed by the same hardware, but can use different caching paths.
Global memory is the general read-write accessible memory visible to all threads
on a given device. Since variables located in global memory have to be marked
with the ``__device__`` qualifier, this memory space is also referred to as
device memory.
Without explicitly copying it, it can only be accessed by the threads within a
kernel operating on the device, however :ref:`unified_memory` can be used to
let the runtime manage this, if desired.
Allocating global memory
--------------------------------------------------------------------------------
This memory needs to be explicitly allocated.
It can be allocated from the host via the :ref:`HIP runtime memory management
functions <memory_management_reference>` like :cpp:func:`hipMalloc`, or can be
defined using the ``__device__`` qualifier on variables.
It can also be allocated within a kernel using ``malloc`` or ``new``.
The specified amount of memory is allocated by each thread that executes the
instructions. The recommended way to allocate the memory depends on the use
case. If the memory is intended to be shared between the threads of a block, it
is generally beneficial to allocate one large block of memory, due to the way
the memory is accessed.
.. note::
Memory allocated within a kernel can only be freed in kernels, not by the HIP
runtime on the host, like :cpp:func:`hipFree`. It is also not possible to
free device memory allocated on the host, with :cpp:func:`hipMalloc` for
example, in a kernel.
An example for how to share memory allocated within a kernel by only one thread
is given in the following example. In case the device memory is only needed for
communication between the threads in a single block, :ref:`shared_memory` is the
better option, but is also limited in size.
.. code-block:: cpp
__global__ void kernel_memory_allocation(TYPE* pointer){
// The pointer is stored in shared memory, so that all
// threads of the block can access the pointer
__shared__ int *memory;
size_t blockSize = blockDim.x;
constexpr size_t elementsPerThread = 1024;
if(threadIdx.x == 0){
// allocate memory in one contiguous block
memory = new int[blockDim.x * elementsPerThread];
}
__syncthreads();
// load pointer into thread-local variable to avoid
// unnecessary accesses to shared memory
int *localPtr = memory;
// work with allocated memory, e.g. initialization
for(int i = 0; i < elementsPerThread; ++i){
// access in a contiguous way
localPtr[i * blockSize + threadIdx.x] = i;
}
// synchronize to make sure no thread is accessing the memory before freeing
__syncthreads();
if(threadIdx.x == 0){
delete[] memory;
}
}
Copying between device and host
--------------------------------------------------------------------------------
When not using :ref:`unified_memory`, memory has to be explicitly copied between
the device and the host, using the HIP runtime API.
.. code-block:: cpp
size_t elements = 1 << 20;
size_t size_bytes = elements * sizeof(int);
// allocate host and device memory
int *host_pointer = new int[elements];
int *device_input, *device_result;
HIP_CHECK(hipMalloc(&device_input, size_bytes));
HIP_CHECK(hipMalloc(&device_result, size_bytes));
// copy from host to the device
HIP_CHECK(hipMemcpy(device_input, host_pointer, size_bytes, hipMemcpyHostToDevice));
// Use memory on the device, i.e. execute kernels
// copy from device to host, to e.g. get results from the kernel
HIP_CHECK(hipMemcpy(host_pointer, device_result, size_bytes, hipMemcpyDeviceToHost));
// free memory when not needed any more
HIP_CHECK(hipFree(device_result));
HIP_CHECK(hipFree(device_input));
delete[] host_pointer;
Constant memory
================================================================================
Read-only storage visible to all threads on a given device. It is a limited
segment backed by device memory with queryable size. It needs to be set by the
host before kernel execution. Constant memory provides the best performance
benefit when all threads within a warp access the same address.
Constant memory is read-only storage visible to all threads on a given device.
It is a limited segment backed by device memory, that takes a different caching
route than normal device memory accesses. It needs to be set by the host before
kernel execution.
In order to get the highest bandwidth from the constant memory, all threads of
a warp have to access the same memory address. If they access different
addresses, the accesses get serialized and the bandwidth is therefore reduced.
Using constant memory
--------------------------------------------------------------------------------
Constant memory can not be dynamically allocated, and the size has to be
specified during compile time. If the values can not be specified during compile
time, they have to be set by the host before the kernel, that accesses the
constant memory, is called.
.. code-block:: cpp
constexpr size_t const_array_size = 32;
__constant__ double const_array[const_array_size];
void set_constant_memory(double* values){
hipMemcpyToSymbol(const_array, values, const_array_size * sizeof(double));
}
__global__ void kernel_using_const_memory(double* array){
int warpIdx = threadIdx.x / warpSize;
// uniform access of warps to const_array for best performance
array[blockDim.x] *= const_array[warpIdx];
}
Texture memory
================================================================================
Read-only storage visible to all threads on a given device and accessible
through additional APIs. Its origins come from graphics APIs, and provides
performance benefits when accessing memory in a pattern where the
addresses are close to each other in a 2D representation of the memory.
Texture memory is special read-only memory visible to all threads on a given
device and accessible through additional APIs. Its origins come from graphics
APIs, and provides performance benefits when accessing memory in a pattern where
the addresses are close to each other in a 2D or 3D representation of the
memory. It also provides additional features like filtering and addressing for
out-of-bounds accesses, which are further explained in :ref:`texture_fetching`.
The :ref:`texture management module <texture_management_reference>` of the HIP
runtime API reference contains the functions of texture memory.
The original use of the texture cache was also to take pressure off the global
memory and other caches, however on modern GPUs, that support textures, the L1
cache and texture cache are combined, so the main purpose is to make use of the
texture specific features.
To find out whether textures are supported on a device, query
:cpp:enumerator:`hipDeviceAttributeImageSupport`.
Using texture memory
--------------------------------------------------------------------------------
Textures are more complex than just a region of memory, so their layout has to
be specified. They are represented by ``hipTextureObject_t`` and created using
:cpp:func:`hipCreateTextureObject`.
The underlying memory is a 1D, 2D or 3D ``hipArray_t``, that needs to be
allocated using :cpp:func:`hipMallocArray`.
On the device side, texture objects are accessed using the ``tex1D/2D/3D``
functions.
The texture management functions can be found in the :ref:`Texture management
API reference <texture_management_reference>`
A full example for how to use textures can be found in the `ROCm texture
management example <https://github.com/ROCm/rocm-examples/blob/develop/HIP-Basic/texture_management/main.hip>`_
Surface memory
================================================================================
A read-write version of texture memory, which can be useful for applications
that require direct manipulation of 1D, 2D, or 3D hipArray_t.
A read-write version of texture memory. It is created in the same way as a
texture, but with :cpp:func:`hipCreateSurfaceObject`.
Since surfaces are also cached in the read-only texture cache, the changes
written back to the surface can't be observed in the same kernel. A new kernel
has to be launched in order to see the updated surface.
The corresponding functions are listed in the :ref:`Surface object API reference
<surface_object_reference>`.
.. _shared_memory:
Shared memory
================================================================================
Shared memory is read-write memory, that is only visible to the threads within a
block. It is allocated per thread block, and needs to be either statically
allocated at compile time, or can be dynamically allocated when launching the
kernel, but not during kernel execution. Its general use-case is to share
variables between the threads within a block, but can also be used as scratch
pad memory.
Shared memory is not backed by the same physical memory as the other address
spaces. It is on-chip memory local to the :ref:`compute units
<hardware_implementation>`, providing low-latency, high-bandwidth access,
comparable to the L1 cache. It is however limited in size, and as it is
allocated per block, can restrict how many blocks can be scheduled to a compute
unit concurrently, thereby potentially reducing occupancy.
An overview of the size of the local data share (LDS), that backs up shared
memory, is given in the
:doc:`GPU hardware specifications <rocm:reference/gpu-arch-specs>`.
Allocate shared memory
--------------------------------------------------------------------------------
Memory can be dynamically allocated by declaring an ``extern __shared__`` array,
whose size can be set during kernel launch, which can then be accessed in the
kernel.
.. code-block:: cpp
extern __shared__ int dynamic_shared[];
__global__ void kernel(int array1SizeX, int array1SizeY, int array2Size){
// at least (array1SizeX * array1SizeY + array2Size) * sizeof(int) bytes
// dynamic shared memory need to be allocated when the kernel is launched
int* array1 = dynamic_shared;
// array1 is interpreted as 2D of size:
int array1Size = array1SizeX * array1SizeY;
int* array2 = &(array1[array1Size]);
if(threadIdx.x < array1SizeX && threadIdx.y < array1SizeY){
// access array1 with threadIdx.x + threadIdx.y * array1SizeX
}
if(threadIdx.x < array2Size){
// access array2 threadIdx.x
}
}
A more in-depth example on dynamically allocated shared memory can be found in
the `ROCm dynamic shared example
<https://github.com/ROCm/rocm-examples/tree/develop/HIP-Basic/dynamic_shared>`_.
To statically allocate shared memory, just declare it in the kernel. The memory
is allocated per block, not per thread. If the kernel requires more shared
memory than is available to the architecture, the compilation fails.
.. code-block:: cpp
__global__ void kernel(){
__shared__ int array[128];
__shared__ double result;
}
A more in-depth example on statically allocated shared memory can be found in
the `ROCm shared memory example
<https://github.com/ROCm/rocm-examples/tree/develop/HIP-Basic/shared_memory>`_.
The :ref:`surface objects module <surface_object_reference>` of HIP runtime API
contains the functions for creating, destroying and reading surface memory.
@@ -5,56 +5,67 @@
.. _texture_fetching:
*******************************************************************************
********************************************************************************
Texture fetching
*******************************************************************************
********************************************************************************
`Textures <../../../../doxygen/html/group___texture.html>`_ are more than just a buffer
interpreted as a 1D, 2D, or 3D array.
Textures give access to specialized hardware on GPUs that is usually used in
graphics processing. In particular, textures use a different way of accessing
their underlying device memory. Memory accesses to textures are routed through
a special read-only texture cache, that is optimized for logical spatial
locality, e.g. locality in 2D grids. This can also benefit certain algorithms
used in GPGPU computing, when the access pattern is the same as used when
accessing normal textures.
As textures are associated with graphics, they are indexed using floating-point
values. The index can be in the range of [0 to size-1] or [0 to 1].
Additionally, textures can be indexed using floating-point values. This is used
in graphics applications to interpolate between neighboring values of a texture.
Depending on the interpolation mode the index can be in the range of ``0`` to
``size - 1`` or ``0`` to ``1``. Textures also have a way of handling
out-of-bounds accesses.
Depending on the index, texture sampling or texture addressing is performed,
which decides the return value.
Depending on the value of the index, :ref:`texture filtering <texture_filtering>`
or :ref:`texture addressing <texture_addressing>` is performed.
**Texture sampling**: When a texture is indexed with a fraction, the queried
value is often between two or more texels (texture elements). The sampling
method defines what value to return in such cases.
**Texture addressing**: Sometimes, the index is outside the bounds of the
texture. This condition might look like a problem but helps to put a texture on
a surface multiple times or to create a visible sign of out-of-bounds indexing,
in computer graphics. The addressing mode defines what value to return when
indexing a texture out of bounds.
The different sampling and addressing modes are described in the following
sections.
Here is the sample texture used in this document for demonstration purposes. It
Here is the example texture used in this document for demonstration purposes. It
is 2x2 texels and indexed in the [0 to 1] range.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/original.png
:width: 150
:alt: Sample texture
:alt: Example texture
:align: center
Texture used as example
Texture sampling
===============================================================================
In HIP textures objects are of type :cpp:struct:`hipTextureObject_t` and created
using :cpp:func:`hipCreateTextureObject`.
Texture sampling handles the usage of fractional indices. It is the method that
describes, which nearby values will be used, and how they are combined into the
resulting value.
For a full list of available texture functions see the :ref:`HIP texture API
reference <texture_management_reference>`.
The various texture sampling methods are discussed in the following sections.
A code example for how to use textures can be found in the `ROCm texture
management example <https://github.com/ROCm/rocm-examples/blob/develop/HIP-Basic/texture_management/main.hip>`_
.. _texture_filtering:
Texture filtering
================================================================================
Texture filtering handles the usage of fractional indices. When the index is a
fraction, the queried value lies between two or more texels (texture elements),
depending on the dimensionality of the texture. The filtering method defines how
to interpolate between these values.
The filter modes are specified in :cpp:enumerator:`hipTextureFilterMode`.
The various texture filtering methods are discussed in the following sections.
.. _texture_fetching_nearest:
Nearest point sampling
Nearest point filtering
-------------------------------------------------------------------------------
This filter mode corresponds to ``hipFilterModePoint``.
In this method, the modulo of index is calculated as:
``tex(x) = T[floor(x)]``
@@ -70,22 +81,24 @@ of the nearest texel.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/nearest.png
:width: 300
:alt: Texture upscaled with nearest point sampling
:alt: Texture upscaled with nearest point filtering
:align: center
Texture upscaled with nearest point sampling
Texture upscaled with nearest point filtering
.. _texture_fetching_linear:
Linear filtering
-------------------------------------------------------------------------------
This filter mode corresponds to ``hipFilterModeLinear``.
The linear filtering method does a linear interpolation between values. Linear
interpolation is used to create a linear transition between two values. The
formula used is ``(1-t)P1 + tP2`` where ``P1`` and ``P2`` are the values and
``t`` is within the [0 to 1] range.
In the case of texture sampling the following formulas are used:
In the case of linear texture filtering the following formulas are used:
* For one dimensional textures: ``tex(x) = (1-α)T[i] + αT[i+1]``
* For two dimensional textures: ``tex(x,y) = (1-α)(1-β)T[i,j] + α(1-β)T[i+1,j] + (1-α)βT[i,j+1] + αβT[i+1,j+1]``
@@ -95,7 +108,7 @@ Where x, y, and, z are the floating-point indices. i, j, and, k are the integer
indices and, α, β, and, γ values represent how far along the sampled point is on
the three axes. These values are calculated by these formulas: ``i = floor(x')``, ``α = frac(x')``, ``x' = x - 0.5``, ``j = floor(y')``, ``β = frac(y')``, ``y' = y - 0.5``, ``k = floor(z')``, ``γ = frac(z')`` and ``z' = z - 0.5``
This following image shows a texture stretched out to a 4x4 pixel quad, but
The following image shows a texture stretched out to a 4x4 pixel quad, but
still indexed in the [0 to 1] range. The in-between values are interpolated
between the neighboring texels.
@@ -106,12 +119,18 @@ between the neighboring texels.
Texture upscaled with linear filtering
.. _texture_addressing:
Texture addressing
===============================================================================
Texture addressing mode handles the index that is out of bounds of the texture.
This mode describes which values of the texture or a preset value to use when
the index is out of bounds.
The texture addressing modes are specified in
:cpp:enumerator:`hipTextureAddressMode`.
The texture addressing mode handles out-of-bounds accesses to the texture. This
can be used in graphics applications to e.g. repeat a texture on a surface
multiple times in various ways or create visible signs of out-of-bounds
indexing.
The following sections describe the various texture addressing methods.
@@ -120,8 +139,10 @@ The following sections describe the various texture addressing methods.
Address mode border
-------------------------------------------------------------------------------
In this method, the texture fetching returns a border value when indexing out of
bounds. The border value must be set before texture fetching.
This addressing mode is set using ``hipAddressModeBorder``.
This addressing mode returns a border value when indexing out of bounds. The
border value must be set before texture fetching.
The following image shows the texture on a 4x4 pixel quad, indexed in the
[0 to 3] range. The out-of-bounds values are the border color, which is yellow.
@@ -141,6 +162,8 @@ the addressing begins.
Address mode clamp
-------------------------------------------------------------------------------
This addressing mode is set using ``hipAddressModeClamp``.
This mode clamps the index between [0 to size-1]. Due to this, when indexing
out-of-bounds, the values on the edge of the texture repeat. The clamp mode is
the default addressing mode.
@@ -164,6 +187,8 @@ the addressing begins.
Address mode wrap
-------------------------------------------------------------------------------
This addressing mode is set using ``hipAddressModeWrap``.
Wrap mode addressing is only available for normalized texture coordinates. In
this addressing mode, the fractional part of the index is used:
@@ -189,6 +214,8 @@ the addressing begins.
Address mode mirror
-------------------------------------------------------------------------------
This addressing mode is set using ``hipAddressModeMirror``.
Similar to the wrap mode the mirror mode is only available for normalized
texture coordinates and also creates a repeating image, but mirroring the
neighboring instances.
@@ -111,8 +111,7 @@ allocator can be used.
❌: **Unsupported**
:sup:`1` Works only with ``HSA_XNACK=1`` and kernels with HMM support. First GPU
access causes recoverable page-fault. For more details, visit `GPU memory
<https://rocm.docs.amd.com/en/latest/conceptual/gpu-memory.html#xnack>`_.
access causes recoverable page-fault.
.. _unified memory allocators:
@@ -144,8 +143,7 @@ GPUs, it is essential to set the environment variable ``HSA_XNACK=1`` and use
a GPU kernel mode driver that supports HMM
<https://www.kernel.org/doc/html/latest/mm/hmm.html>`_. Without this
configuration, the behavior will be similar to that of systems without HMM
support. For more details, visit
`GPU memory <https://rocm.docs.amd.com/en/latest/conceptual/gpu-memory.html#xnack>`_.
support.
The table below illustrates the expected behavior of managed and unified memory
functions on ROCm and CUDA, both with and without HMM support.
@@ -28,7 +28,7 @@ reduce memory usage and unnecessary ``memcpy`` calls.
.. _memory_allocation_virtual_memory:
Memory allocation
================================================================================
=================
Standard memory allocation uses the :cpp:func:`hipMalloc` function to allocate a
block of memory on the device. However, when using virtual memory, this process
@@ -37,10 +37,34 @@ is separated into multiple steps using the :cpp:func:`hipMemCreate`,
:cpp:func:`hipMemSetAccess` functions. This guide explains what these functions
do and how you can use them for virtual memory management.
Allocate physical memory
--------------------------------------------------------------------------------
.. _vmm_support:
The first step is to allocate the physical memory itself with the
Virtual memory management support
---------------------------------
The first step is to check if the targeted device or GPU supports virtual memory management.
Use the :cpp:func:`hipDeviceGetAttribute` function to get the
``hipDeviceAttributeVirtualMemoryManagementSupported`` attribute for a specific GPU, as shown in the following example.
.. code-block:: cpp
int vmm = 0, currentDev = 0;
hipDeviceGetAttribute(
&vmm, hipDeviceAttributeVirtualMemoryManagementSupported, currentDev
);
if (vmm == 0) {
std::cout << "GPU " << currentDev << " doesn't support virtual memory management." << std::endl;
} else {
std::cout << "GPU " << currentDev << " support virtual memory management." << std::endl;
}
.. _allocate_physical_memory:
Allocate physical memory
------------------------
The next step is to allocate the physical memory using the
:cpp:func:`hipMemCreate` function. This function accepts the size of the buffer,
an ``unsigned long long`` variable for the flags, and a
:cpp:struct:`hipMemAllocationProp` variable. :cpp:struct:`hipMemAllocationProp`
@@ -48,42 +72,54 @@ contains the properties of the memory to be allocated, such as where the memory
is physically located and what kind of shareable handles are available. If the
allocation is successful, the function returns a value of
:cpp:enumerator:`hipSuccess`, with :cpp:type:`hipMemGenericAllocationHandle_t`
representing a valid physical memory allocation. The allocated memory size must
be aligned with the granularity appropriate for the properties of the
allocation. You can use the :cpp:func:`hipMemGetAllocationGranularity` function
to determine the correct granularity.
representing a valid physical memory allocation.
The allocated memory must be aligned with the appropriate granularity. The
granularity value can be queried with :cpp:func:`hipMemGetAllocationGranularity`,
and its value depends on the target device hardware and the type of memory
allocation. If the allocation size is not aligned, meaning it is not cleanly
divisible by the minimum granularity value, :cpp:func:`hipMemCreate` will return
an out-of-memory error.
.. code-block:: cpp
size_t granularity = 0;
hipMemGenericAllocationHandle_t allocHandle;
hipMemAllocationProp prop = {};
prop.type = HIP_MEM_ALLOCATION_TYPE_PINNED;
prop.location.type = HIP_MEM_LOCATION_TYPE_DEVICE;
// The pinned allocation type cannot be migrated from its current location
// while the application is actively using it.
prop.type = hipMemAllocationTypePinned;
// Set the location type to device, currently there are no other valid option.
prop.location.type = hipMemLocationTypeDevice;
// Set the device id, where the memory will be allocated.
prop.location.id = currentDev;
hipMemGetAllocationGranularity(&granularity, &prop, HIP_MEM_ALLOC_GRANULARITY_MINIMUM);
hipMemGetAllocationGranularity(&granularity, &prop, hipMemAllocationGranularityMinimum);
padded_size = ROUND_UP(size, granularity);
hipMemCreate(&allocHandle, padded_size, &prop, 0);
Reserve virtual address range
--------------------------------------------------------------------------------
.. _reserve_virtual_address:
After you have acquired an allocation of physical memory, you must map it before
you can use it. To do so, you need a virtual address to map it to. Mapping
means the physical memory allocation is available from the virtual address range
it is mapped to. To reserve a virtual memory range, use the
:cpp:func:`hipMemAddressReserve` function. The size of the virtual memory must
match the amount of physical memory previously allocated. You can then map the
physical memory allocation to the newly-acquired virtual memory address range
using the :cpp:func:`hipMemMap` function.
Reserve virtual address range
-----------------------------
After you have acquired an allocation of physical memory, you must map it to a
virtual address before you can use it. Mapping means the physical memory
allocation is available from the virtual address range it is mapped to. To
reserve a virtual memory range, use the :cpp:func:`hipMemAddressReserve`
function. The size of the virtual memory must match the amount of physical
memory previously allocated. You can then map the physical memory allocation to
the newly-acquired virtual memory address range using the :cpp:func:`hipMemMap`
function.
.. code-block:: cpp
hipMemAddressReserve(&ptr, padded_size, 0, 0, 0);
hipMemMap(ptr, padded_size, 0, allocHandle, 0);
.. _set_memory_access:
Set memory access
--------------------------------------------------------------------------------
-----------------
Finally, use the :cpp:func:`hipMemSetAccess` function to enable memory access.
It accepts the pointer to the virtual memory, the size, and a
@@ -103,16 +139,39 @@ devices.
.. code-block:: cpp
hipMemAccessDesc accessDesc = {};
accessDesc.location.type = HIP_MEM_LOCATION_TYPE_DEVICE;
accessDesc.location.type = hipMemLocationTypeDevice;
accessDesc.location.id = currentDev;
accessDesc.flags = HIP_MEM_ACCESS_FLAGS_PROT_READWRITE;
accessDesc.flags = hipMemAccessFlagsProtReadwrite;
hipMemSetAccess(ptr, padded_size, &accessDesc, 1);
At this point the memory is allocated, mapped, and ready for use. You can read
and write to it, just like you would a C style memory allocation.
.. _usage_virtual_memory:
Dynamically increase allocation size
------------------------------------
To increase the amount of pre-allocated memory, use
:cpp:func:`hipMemAddressReserve`, which accepts the starting address, and the
size of the reservation in bytes. This allows you to have a continuous virtual
address space without worrying about the underlying physical allocation.
.. code-block:: cpp
hipMemAddressReserve(&new_ptr, (new_size - padded_size), 0, ptr + padded_size, 0);
hipMemMap(new_ptr, (new_size - padded_size), 0, newAllocHandle, 0);
hipMemSetAccess(new_ptr, (new_size - padded_size), &accessDesc, 1);
The code sample above assumes that :cpp:func:`hipMemAddressReserve` was able to
reserve the memory address at the specified location. However, this isn't
guaranteed to be true, so you should validate that ``new_ptr`` points to a
specific virtual address before using it.
.. _free_virtual_memory:
Free virtual memory
--------------------------------------------------------------------------------
-------------------
To free the memory allocated in this manner, use the corresponding free
functions. To unmap the memory, use :cpp:func:`hipMemUnmap`. To release the
@@ -128,27 +187,197 @@ synchronizes the device. This causes worse resource usage and performance.
hipMemRelease(allocHandle);
hipMemAddressFree(ptr, size);
.. _usage_virtual_memory:
Example code
============
Memory usage
================================================================================
The virtual memory management example follows these steps:
Dynamically increase allocation size
--------------------------------------------------------------------------------
1. Check virtual memory management :ref:`support <vmm_support>`:
The :cpp:func:`hipDeviceGetAttribute` function is used to check the virtual
memory management support of the GPU with ID 0.
The :cpp:func:`hipMemAddressReserve` function allows you to increase the amount
of pre-allocated memory. This function accepts a parameter representing the
requested starting address of the virtual memory. This allows you to have a
continuous virtual address space without worrying about the underlying physical
allocation.
2. Physical memory :ref:`allocation <allocate_physical_memory>`: Physical memory
is allocated using :cpp:func:`hipMemCreate` with pinned memory on the
device.
3. Virtual memory :ref:`reservation <reserve_virtual_address>`: Virtual address
range is reserved using :cpp:func:`hipMemAddressReserve`.
4. Mapping virtual address to physical memory: The physical memory is mapped
to a virtual address (``virtualPointer``) using :cpp:func:`hipMemMap`.
5. Memory :ref:`access permissions<set_memory_access>`: Permission is set for
pointer to allow read and write access using :cpp:func:`hipMemSetAccess`.
6. Memory operation: Data is written to the memory via ``virtualPointer``.
7. Launch kernels: The ``zeroAddr`` and ``fillAddr`` kernels are
launched using the virtual memory pointer.
8. :ref:`Cleanup <free_virtual_memory>`: The mappings, physical memory, and
virtual address are released at the end to avoid memory leaks.
.. code-block:: cpp
hipMemAddressReserve(&new_ptr, (new_size - padded_size), 0, ptr + padded_size, 0);
hipMemMap(new_ptr, (new_size - padded_size), 0, newAllocHandle, 0);
hipMemSetAccess(new_ptr, (new_size - padded_size), &accessDesc, 1);
#include <hip/hip_runtime.h>
#include <iostream>
#define ROUND_UP(SIZE,GRANULARITY) ((1 + SIZE / GRANULARITY) * GRANULARITY)
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
__global__ void zeroAddr(int* pointer) {
*pointer = 0;
}
__global__ void fillAddr(int* pointer) {
*pointer = 42;
}
int main() {
int currentDev = 0;
// Step 1: Check virtual memory management support on device 0
int vmm = 0;
HIP_CHECK(
hipDeviceGetAttribute(
&vmm, hipDeviceAttributeVirtualMemoryManagementSupported, currentDev
)
);
std::cout << "Virtual memory management support value: " << vmm << std::endl;
if (vmm == 0) {
std::cout << "GPU 0 doesn't support virtual memory management.";
return 0;
}
// Size of memory to allocate
size_t size = 4 * 1024;
// Step 2: Allocate physical memory
hipMemGenericAllocationHandle_t allocHandle;
hipMemAllocationProp prop = {};
prop.type = hipMemAllocationTypePinned;
prop.location.type = hipMemLocationTypeDevice;
prop.location.id = currentDev;
size_t granularity = 0;
HIP_CHECK(
hipMemGetAllocationGranularity(
&granularity,
&prop,
hipMemAllocationGranularityMinimum));
size_t padded_size = ROUND_UP(size, granularity);
HIP_CHECK(hipMemCreate(&allocHandle, padded_size * 2, &prop, 0));
// Step 3: Reserve a virtual memory address range
void* virtualPointer = nullptr;
HIP_CHECK(hipMemAddressReserve(&virtualPointer, padded_size, granularity, nullptr, 0));
// Step 4: Map the physical memory to the virtual address range
HIP_CHECK(hipMemMap(virtualPointer, padded_size, 0, allocHandle, 0));
// Step 5: Set memory access permission for pointer
hipMemAccessDesc accessDesc = {};
accessDesc.location.type = hipMemLocationTypeDevice;
accessDesc.location.id = currentDev;
accessDesc.flags = hipMemAccessFlagsProtReadWrite;
HIP_CHECK(hipMemSetAccess(virtualPointer, padded_size, &accessDesc, 1));
// Step 6: Perform memory operation
int value = 42;
HIP_CHECK(hipMemcpy(virtualPointer, &value, sizeof(int), hipMemcpyHostToDevice));
int result = 1;
HIP_CHECK(hipMemcpy(&result, virtualPointer, sizeof(int), hipMemcpyDeviceToHost));
if( result == 42) {
std::cout << "Success. Value: " << result << std::endl;
} else {
std::cout << "Failure. Value: " << result << std::endl;
}
// Step 7: Launch kernels
// Launch zeroAddr kernel
zeroAddr<<<1, 1>>>((int*)virtualPointer);
HIP_CHECK(hipDeviceSynchronize());
// Check zeroAddr kernel result
result = 1;
HIP_CHECK(hipMemcpy(&result, virtualPointer, sizeof(int), hipMemcpyDeviceToHost));
if( result == 0) {
std::cout << "Success. zeroAddr kernel: " << result << std::endl;
} else {
std::cout << "Failure. zeroAddr kernel: " << result << std::endl;
}
// Launch fillAddr kernel
fillAddr<<<1, 1>>>((int*)virtualPointer);
HIP_CHECK(hipDeviceSynchronize());
// Check fillAddr kernel result
result = 1;
HIP_CHECK(hipMemcpy(&result, virtualPointer, sizeof(int), hipMemcpyDeviceToHost));
if( result == 42) {
std::cout << "Success. fillAddr kernel: " << result << std::endl;
} else {
std::cout << "Failure. fillAddr kernel: " << result << std::endl;
}
// Step 8: Cleanup
HIP_CHECK(hipMemUnmap(virtualPointer, padded_size));
HIP_CHECK(hipMemRelease(allocHandle));
HIP_CHECK(hipMemAddressFree(virtualPointer, padded_size));
return 0;
}
Virtual aliases
================================================================================
Virtual aliases are multiple virtual memory addresses mapping to the same
physical memory on the GPU. When this occurs, different threads, processes, or memory
allocations to access shared physical memory through different virtual
addresses on different devices.
Multiple virtual memory mappings can be created using multiple calls to
:cpp:func:`hipMemMap` on the same memory allocation.
.. note::
RDNA cards may not produce correct results, if users access two different
virtual addresses that map to the same physical address. In this case, the
L1 data caches will be incoherent due to the virtual-to-physical aliasing.
These GPUs will produce correct results if users access virtual-to-physical
aliases using volatile pointers.
NVIDIA GPUs require special fences to produce correct results when
using virtual aliases.
In the following code block, the kernels input device pointers are virtual
aliases of the same memory allocation:
.. code-block:: cpp
__global__ void updateBoth(int* pointerA, int* pointerB) {
// May produce incorrect results on RDNA and NVIDIA cards.
*pointerA = 0;
*pointerB = 42;
}
__global__ void updateBoth_v2(volatile int* pointerA, volatile int* pointerB) {
// May produce incorrect results on NVIDIA cards.
*pointerA = 0;
*pointerB = 42;
}
The code sample above assumes that :cpp:func:`hipMemAddressReserve` was able to
reserve the memory address at the specified location. However, this isn't
guaranteed to be true, so you should validate that ``new_ptr`` points to a
specific virtual address before using it.