SWDEV-502480 - Update documentation from GitHub 2024-12-05

Change-Id: I179814351b77935aff55e8ae47dd322a3e15a868
Tento commit je obsažen v:
Istvan Kiss
2024-12-15 19:31:35 +01:00
rodič 15e2512f02
revize f39c7a3150
85 změnil soubory, kde provedl 8331 přidání a 3512 odebrání
+178
Zobrazit soubor
@@ -0,0 +1,178 @@
.. meta::
:description: HIP coherence control
ecosystem ROCm software.
:keywords: AMD, ROCm, HIP, host memory
.. _coherence_control:
*******************************************************************************
Coherence control
*******************************************************************************
Memory coherence describes how memory of a specific part of the system is
visible to the other parts of the system. For example, how GPU memory is visible
to the CPU and vice versa. In HIP, host and device memory can be allocated with
two different types of coherence:
* **Coarse-grained coherence:** The memory is considered up-to-date only after
synchronization performed using :cpp:func:`hipDeviceSynchronize`,
:cpp:func:`hipStreamSynchronize`, or any blocking operation that acts on the
null stream such as :cpp:func:`hipMemcpy`. To avoid the cache from being
accessed by a part of the system while simultaneously being written by
another, the memory is made visible only after the caches have been flushed.
* **Fine-grained coherence:** The memory is coherent even while being modified
by a part of the system. Fine-grained coherence ensures that up-to-date data
is visible to others regardless of kernel boundaries. This can be useful if
both host and device operate on the same data.
.. note::
To achieve fine-grained coherence, many AMD GPUs use a limited cache policy,
such as leaving these allocations uncached by the GPU or making them read-only.
Mi200 accelerator's hardware based floating point instructions work on
coarse-grained memory regions. Coarse-grained coherence is typically useful in
reducing host-device interconnect communication.
To check the availability of fine- and coarse-grained memory pools, use
``rocminfo``:
.. code-block:: sh
$ rocminfo
...
*******
Agent 1
*******
Name: AMD EPYC 7742 64-Core Processor
...
Pool Info:
Pool 1
Segment: GLOBAL; FLAGS: FINE GRAINED
...
Pool 3
Segment: GLOBAL; FLAGS: COARSE GRAINED
...
*******
Agent 9
*******
Name: gfx90a
...
Pool Info:
Pool 1
Segment: GLOBAL; FLAGS: COARSE GRAINED
...
The APIs, flags and respective memory coherence control are listed in the
following table:
.. list-table:: Memory coherence control
:widths: 25, 35, 20, 20
:header-rows: 1
:align: center
* - API
- Flag
- :cpp:func:`hipMemAdvise` call with argument
- Coherence
* - ``hipHostMalloc`` :sup:`1`
- ``hipHostMallocDefault``
-
- Fine-grained
* - ``hipHostMalloc`` :sup:`1`
- ``hipHostMallocNonCoherent``
-
- Coarse-grained
* - ``hipExtMallocWithFlags``
- ``hipDeviceMallocDefault``
-
- Coarse-grained
* - ``hipExtMallocWithFlags``
- ``hipDeviceMallocFinegrained``
-
- Fine-grained
* - ``hipMallocManaged``
-
-
- Fine-grained
* - ``hipMallocManaged``
-
- ``hipMemAdviseSetCoarseGrain``
- Coarse-grained
* - ``malloc``
-
-
- Fine-grained
* - ``malloc``
-
- ``hipMemAdviseSetCoarseGrain``
- Coarse-grained
:sup:`1` The :cpp:func:`hipHostMalloc` memory allocation coherence mode can be
affected by the ``HIP_HOST_COHERENT`` environment variable, if the
``hipHostMallocCoherent``, ``hipHostMallocNonCoherent``, and
``hipHostMallocMapped`` are unset. If neither these flags nor the
``HIP_HOST_COHERENT`` environment variable is set, or set as 0, the host memory
allocation is coarse-grained.
.. note::
* When ``hipHostMallocMapped`` flag is set, the allocated host memory is
fine-grained and the ``hipHostMallocNonCoherent`` flag is ignored.
* Setting both the ``hipHostMallocCoherent`` and
``hipHostMallocNonCoherent`` flags leads to an illegal state.
Visibility of synchronization functions
================================================================================
The fine-grained coherence memory is visible at the synchronization points,
however the visibility of coarse-grained memory depends on the synchronization
function used. The effect and visibility of various synchronization functions on
fine- and coarse-grained memory types are listed here:
.. list-table:: HIP synchronize functions effect and visibility
* - HIP API
- :cpp:func:`hipStreamSynchronize`
- :cpp:func:`hipDeviceSynchronize`
- :cpp:func:`hipEventSynchronize`
- :cpp:func:`hipStreamWaitEvent`
* - Synchronization effect
- Host waits for all commands in the specified stream to complete
- Host waits for all commands in all streams on the specified device to complete
- Host waits for the specified event to complete
- Stream waits for the specified event to complete
* - Fence
- System-scope release
- System-scope release
- System-scope release
- None
* - Fine-grained host memory visibility
- Yes
- Yes
- Yes
- Yes
* - Coarse-grained host memory visibility
- Yes
- Yes
- Depends on the used event.
- No
You can control the release scope for ``hipEvents``. By default, the GPU
performs a device-scope acquire and release operation with each recorded event.
This makes the host and device memory visible to other commands executing on the
same device.
:cpp:func:`hipEventCreateWithFlags`: You can specify a stronger system-level
fence by creating the event with ``hipEventCreateWithFlags``:
* ``hipEventReleaseToSystem``: Performs a system-scope release operation when
the event is recorded. This makes both fine-grained and coarse-grained host
memory visible to other agents in the system, which might also involve
heavyweight operations such as cache flushing. Fine-grained memory typically
uses lighter-weight in-kernel synchronization mechanisms such as an atomic
operation and thus doesn't need to use ``hipEventReleaseToSystem``.
* ``hipEventDisableTiming``: Events created with this flag don't record
profiling data, which significantly improves synchronization performance.
+52
Zobrazit soubor
@@ -0,0 +1,52 @@
.. meta::
:description: This chapter describes the device memory of the HIP ecosystem
ROCm software.
:keywords: AMD, ROCm, HIP, device memory
.. _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.
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.
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.
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.
The :ref:`texture management module <texture_management_reference>` of the HIP
runtime API reference contains the functions of texture memory.
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.
The :ref:`surface objects module <surface_object_reference>` of HIP runtime API
contains the functions for creating, destroying and reading surface memory.
@@ -0,0 +1,214 @@
.. meta::
:description: This chapter describes the texture fetching modes of the HIP ecosystem
ROCm software.
:keywords: AMD, ROCm, HIP, Texture, Texture Fetching
.. _texture_fetching:
*******************************************************************************
Texture fetching
*******************************************************************************
`Textures <../../../../doxygen/html/group___texture.html>`_ are more than just a buffer
interpreted as a 1D, 2D, or 3D array.
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].
Depending on the index, texture sampling or texture addressing is performed,
which decides the return value.
**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
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
:align: center
Texture used as example
Texture sampling
===============================================================================
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.
The various texture sampling methods are discussed in the following sections.
.. _texture_fetching_nearest:
Nearest point sampling
-------------------------------------------------------------------------------
In this method, the modulo of index is calculated as:
``tex(x) = T[floor(x)]``
This is also applicable for 2D and 3D variants.
This doesn't interpolate between neighboring values, which results in a
pixelated look.
The following image shows a texture stretched to a 4x4 pixel quad but still
indexed in the [0 to 1] range. The in-between values are the same as the values
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
:align: center
Texture upscaled with nearest point sampling
.. _texture_fetching_linear:
Linear filtering
-------------------------------------------------------------------------------
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:
* 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]``
* For three dimensional textures: ``tex(x,y,z) = (1-α)(1-β)(1-γ)T[i,j,k] + α(1-β)(1-γ)T[i+1,j,k] + (1-α)β(1-γ)T[i,j+1,k] + αβ(1-γ)T[i+1,j+1,k] + (1-α)(1-β)γT[i,j,k+1] + α(1-β)γT[i+1,j,k+1] + (1-α)βγT[i,j+1,k+1] + αβγT[i+1,j+1,k+1]``
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
still indexed in the [0 to 1] range. The in-between values are interpolated
between the neighboring texels.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/linear.png
:width: 300
:alt: Texture upscaled with linear filtering
:align: center
Texture upscaled with linear filtering
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 following sections describe the various texture addressing methods.
.. _texture_fetching_border:
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.
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.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/border.png
:width: 300
:alt: Texture with yellow border color
:align: center
Texture with yellow border color.
The purple lines are not part of the texture. They only denote the edge, where
the addressing begins.
.. _texture_fetching_clamp:
Address mode clamp
-------------------------------------------------------------------------------
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.
The following image shows the texture on a 4x4 pixel quad, indexed in the
[0 to 3] range. The out-of-bounds values are repeating the values at the edge of
the texture.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/clamp.png
:width: 300
:alt: Texture with clamp addressing
:align: center
Texture with clamp addressing
The purple lines are not part of the texture. They only denote the edge, where
the addressing begins.
.. _texture_fetching_wrap:
Address mode wrap
-------------------------------------------------------------------------------
Wrap mode addressing is only available for normalized texture coordinates. In
this addressing mode, the fractional part of the index is used:
``tex(frac(x))``
This creates a repeating image effect.
The following image shows the texture on a 4x4 pixel quad, indexed in the
[0 to 3] range. The out-of-bounds values are repeating the original texture.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/wrap.png
:width: 300
:alt: Texture with wrap addressing
:align: center
Texture with wrap addressing.
The purple lines are not part of the texture. They only denote the edge, where
the addressing begins.
.. _texture_fetching_mirror:
Address mode mirror
-------------------------------------------------------------------------------
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.
The formula is the following:
``tex(frac(x))``, if ``floor(x)`` is even,
``tex(1 - frac(x))``, if ``floor(x)`` is odd.
The following image shows the texture on a 4x4 pixel quad, indexed in The
[0 to 3] range. The out-of-bounds values are repeating the original texture, but
mirrored.
.. figure:: ../../../../data/how-to/hip_runtime_api/memory_management/textures/mirror.png
:width: 300
:alt: Texture with mirror addressing
:align: center
Texture with mirror addressing
The purple lines are not part of the texture. They only denote the edge, where
the addressing begins.
+239
Zobrazit soubor
@@ -0,0 +1,239 @@
.. meta::
:description: Host memory of the HIP ecosystem
:keywords: AMD, ROCm, HIP, host memory
.. _host_memory:
********************************************************************************
Host memory
********************************************************************************
Host memory is the "normal" memory residing in the host RAM and allocated by C
or C++. Host memory can be allocated in two different ways:
* Pageable memory
* Pinned memory
The following figure explains how data is transferred in pageable and pinned
memory.
.. figure:: ../../../data/how-to/hip_runtime_api/memory_management/pageable_pinned.svg
The pageable and pinned memory allow you to exercise direct control over
memory operations, which is known as explicit memory management. When using the
unified memory, you get a simplified memory model with less control over
low level memory operations.
The difference in memory transfers between explicit and unified memory
management is highlighted in the following figure:
.. figure:: ../../../data/how-to/hip_runtime_api/memory_management/unified_memory/um.svg
For more details on unified memory management, see :doc:`/how-to/hip_runtime_api/memory_management/unified_memory`.
.. _pageable_host_memory:
Pageable memory
================================================================================
Pageable memory exists on memory blocks known as "pages" that can be migrated to
other memory storage. For example, migrating memory between CPU sockets on a
motherboard or in a system whose RAM runs out of space and starts dumping pages
into the swap partition of the hard drive.
Pageable memory is usually allocated with a call to ``malloc`` or ``new`` in a
C++ application.
**Example:** Using pageable host memory in HIP
.. code-block:: cpp
#include <hip/hip_runtime.h>
#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; \
} \
}
int main()
{
const int element_number = 100;
int *host_input, *host_output;
// Host allocation
host_input = new int[element_number];
host_output = new int[element_number];
// Host data preparation
for (int i = 0; i < element_number; i++) {
host_input[i] = i;
}
memset(host_output, 0, element_number * sizeof(int));
int *device_input, *device_output;
// Device allocation
HIP_CHECK(hipMalloc((int **)&device_input, element_number * sizeof(int)));
HIP_CHECK(hipMalloc((int **)&device_output, element_number * sizeof(int)));
// Device data preparation
HIP_CHECK(hipMemcpy(device_input, host_input, element_number * sizeof(int), hipMemcpyHostToDevice));
HIP_CHECK(hipMemset(device_output, 0, element_number * sizeof(int)));
// Run the kernel
// ...
HIP_CHECK(hipMemcpy(device_input, host_input, element_number * sizeof(int), hipMemcpyHostToDevice));
// Free host memory
delete[] host_input;
delete[] host_output;
// Free device memory
HIP_CHECK(hipFree(device_input));
HIP_CHECK(hipFree(device_output));
}
.. note::
:cpp:func:`hipMalloc` and :cpp:func:`hipFree` are blocking calls. However, HIP
also provides non-blocking versions :cpp:func:`hipMallocAsync` and
:cpp:func:`hipFreeAsync`, which require a stream as an additional argument.
.. _pinned_host_memory:
Pinned memory
================================================================================
Pinned memory or page-locked memory is stored in pages that are locked in
specific sectors in RAM and can't be migrated. The pointer can be used on both
host and device. Accessing host-resident pinned memory in device kernels is
generally not recommended for performance, as it can force the data to traverse
the host-device interconnect such as PCIe, which is much slower than the
on-device bandwidth.
The advantage of pinned memory is the improved transfer time between host and
device. For transfer operations, such as :cpp:func:`hipMemcpy` or :cpp:func:`hipMemcpyAsync`,
using pinned memory instead of pageable memory on the host can lead to a three times
improvement in bandwidth.
The disadvantage of pinned memory is the reduced availability of RAM for other
processes, which can negatively impact the overall performance of the host.
**Example:** Using pinned memory in HIP
.. code-block:: cpp
#include <hip/hip_runtime.h>
#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; \
} \
}
int main()
{
const int element_number = 100;
int *host_input, *host_output;
// Host allocation
HIP_CHECK(hipHostMalloc((int **)&host_input, element_number * sizeof(int)));
HIP_CHECK(hipHostMalloc((int **)&host_output, element_number * sizeof(int)));
// Host data preparation
for (int i = 0; i < element_number; i++) {
host_input[i] = i;
}
memset(host_output, 0, element_number * sizeof(int));
int *device_input, *device_output;
// Device allocation
HIP_CHECK(hipMalloc((int **)&device_input, element_number * sizeof(int)));
HIP_CHECK(hipMalloc((int **)&device_output, element_number * sizeof(int)));
// Device data preparation
HIP_CHECK(hipMemcpy(device_input, host_input, element_number * sizeof(int), hipMemcpyHostToDevice));
HIP_CHECK(hipMemset(device_output, 0, element_number * sizeof(int)));
// Run the kernel
// ...
HIP_CHECK(hipMemcpy(device_input, host_input, element_number * sizeof(int), hipMemcpyHostToDevice));
// Free host memory
delete[] host_input;
delete[] host_output;
// Free device memory
HIP_CHECK(hipFree(device_input));
HIP_CHECK(hipFree(device_output));
}
.. _memory_allocation_flags:
Memory allocation flags for pinned memory
--------------------------------------------------------------------------------
The memory allocation for pinned memory can be controlled using ``hipHostMalloc`` flags:
* ``hipHostMallocPortable``: The memory allocation is not restricted to the
context making the allocation.
* ``hipHostMallocMapped``: The memory is allocated into the address space for
the current device and the device pointer can be obtained with
:cpp:func:`hipHostGetDevicePointer`.
* ``hipHostMallocNumaUser``: The host memory allocation follows Numa policy
specified by the user. Target of Numa policy is to select a CPU that is
closest to each GPU. Numa distance is the distance between GPU and CPU
devices.
* ``hipHostMallocWriteCombined``: The memory is allocated as write-combined.
Although lacking read efficiency by most CPUs, write-combined allocation might
be transferred faster across the PCIe bus on some system configurations. It's
a good option for data transfer from host to device via mapped pinned memory.
* ``hipHostMallocCoherent``: Fine-grained memory is allocated. Overrides
``HIP_HOST_COHERENT`` environment variable for specific allocation. For
details, see :ref:`coherence_control`.
* ``hipHostMallocNonCoherent``: Coarse-grained memory is allocated. Overrides
``HIP_HOST_COHERENT`` environment variable for specific allocation. For
details, see :ref:`coherence_control`.
All allocation flags are independent and can be set in any combination. The only
exception is setting ``hipHostMallocCoherent`` and ``hipHostMallocNonCoherent``
together, which leads to an illegal state. An example of a valid flag
combination is calling :cpp:func:`hipHostMalloc` with both
``hipHostMallocPortable`` and ``hipHostMallocMapped`` flags set. Both the flags
use the same model and differentiate only between how the surrounding code uses
the host memory.
.. note::
By default, each GPU selects a Numa CPU node with the least Numa distance
between them. This implies that the host memory is automatically allocated on
the closest memory pool of the current GPU device's Numa node. Using
:cpp:func:`hipSetDevice` API to set a different GPU increases the Numa
distance but still allows you to access the host allocation.
Numa policy is implemented on Linux and is under development on Microsoft
Windows.
@@ -0,0 +1,580 @@
.. meta::
:description:
:keywords: stream, memory allocation, SOMA, stream ordered memory allocator
.. _stream_ordered_memory_allocator_how-to:
*******************************************************************************
Stream Ordered Memory Allocator
*******************************************************************************
The Stream Ordered Memory Allocator (SOMA) is part of the HIP runtime API. SOMA provides an asynchronous memory allocation mechanism with stream-ordering semantics. You can use SOMA to allocate and free memory in stream order, which ensures that all asynchronous accesses occur between the stream executions of allocation and deallocation. Compliance with stream order prevents use-before-allocation or use-after-free errors, which helps to avoid an undefined behavior.
Advantages of SOMA:
- Efficient reuse: Enables efficient memory reuse across streams, which reduces unnecessary allocation overhead.
- Fine-grained control: Allows you to set attributes and control caching behavior for memory pools.
- Inter-process sharing: Enables secure sharing of allocations between processes.
- Optimizations: Allows driver to optimize based on its awareness of SOMA and other stream management APIs.
Disadvantages of SOMA:
- Temporal constraints: Requires you to adhere strictly to stream order to avoid errors.
- Complexity: Involves memory management in stream order, which can be intricate.
- Learning curve: Requires you to put additional efforts to understand and utilize SOMA effectively.
Using SOMA
=====================================
You can allocate memory using ``hipMallocAsync()`` with stream-ordered
semantics. This restricts the asynchronous access to the memory between the stream executions of the allocation and deallocation. Accessing
memory if the compliant memory accesses won't overlap
temporally. ``hipFreeAsync()`` frees memory from the pool with stream-ordered
semantics.
Here is how to use stream ordered memory allocation:
.. tab-set::
.. tab-item:: Stream Ordered Memory Allocation
.. code-block:: cpp
#include <iostream>
#include <hip/hip_runtime.h>
// Kernel to perform some computation on allocated memory.
__global__ void myKernel(int* data, size_t numElements) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < numElements) {
data[tid] = tid * 2;
}
}
int main() {
// Initialize HIP.
hipInit(0);
// Stream 0.
constexpr hipStream_t streamId = 0;
// Allocate memory with stream ordered semantics.
constexpr size_t numElements = 1024;
int* devData;
hipMallocAsync(&devData, numElements * sizeof(*devData), streamId);
// Launch the kernel to perform computation.
dim3 blockSize(256);
dim3 gridSize((numElements + blockSize.x - 1) / blockSize.x);
myKernel<<<gridSize, blockSize>>>(devData, numElements);
// Copy data back to host.
int* hostData = new int[numElements];
hipMemcpy(hostData, devData, numElements * sizeof(*devData), hipMemcpyDeviceToHost);
// Print the array.
for (size_t i = 0; i < numElements; ++i) {
std::cout << "Element " << i << ": " << hostData[i] << std::endl;
}
// Free memory with stream ordered semantics.
hipFreeAsync(devData, streamId);
delete[] hostData;
// Synchronize to ensure completion.
hipDeviceSynchronize();
return 0;
}
.. tab-item:: Ordinary Allocation
.. code-block:: cpp
#include <iostream>
#include <hip/hip_runtime.h>
// Kernel to perform some computation on allocated memory.
__global__ void myKernel(int* data, size_t numElements) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < numElements) {
data[tid] = tid * 2;
}
}
int main() {
// Initialize HIP.
hipInit(0);
// Allocate memory.
constexpr size_t numElements = 1024;
int* devData;
hipMalloc(&devData, numElements * sizeof(*devData));
// Launch the kernel to perform computation.
dim3 blockSize(256);
dim3 gridSize((numElements + blockSize.x - 1) / blockSize.x);
myKernel<<<gridSize, blockSize>>>(devData, numElements);
// Copy data back to host.
int* hostData = new int[numElements];
hipMemcpy(hostData, devData, numElements * sizeof(*devData), hipMemcpyDeviceToHost);
// Print the array.
for (size_t i = 0; i < numElements; ++i) {
std::cout << "Element " << i << ": " << hostData[i] << std::endl;
}
// Free memory.
hipFree(devData);
delete[] hostData;
// Synchronize to ensure completion.
hipDeviceSynchronize();
return 0;
}
For more details, see :ref:`stream_ordered_memory_allocator_reference`.
Memory pools
============
Memory pools provide a way to manage memory with stream-ordered behavior while ensuring proper synchronization and avoiding memory access errors. Division of a single memory system into separate pools facilitates querying the access path properties for each partition. Memory pools are used for host memory, device memory, and unified memory.
Set pools
---------
The ``hipMallocAsync()`` function uses the current memory pool and also provides the opportunity to create and access different pools using ``hipMemPoolCreate()`` and ``hipMallocFromPoolAsync()`` functions respectively.
Unlike NVIDIA CUDA, where stream-ordered memory allocation can be implicit, ROCm HIP is explicit. This requires managing memory allocation for each stream in HIP while ensuring precise control over memory usage and synchronization.
.. code-block:: cpp
#include <iostream>
#include <hip/hip_runtime.h>
// Kernel to perform some computation on allocated memory.
__global__ void myKernel(int* data, size_t numElements) {
int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < numElements) {
data[tid] = tid * 2;
}
}
int main() {
// Create a stream.
hipStream_t stream;
hipStreamCreate(&stream);
// Create a memory pool with default properties.
hipMemPoolProps poolProps = {};
poolProps.allocType = hipMemAllocationTypePinned;
poolProps.handleTypes = hipMemHandleTypePosixFileDescriptor;
poolProps.location.type = hipMemLocationTypeDevice;
poolProps.location.id = 0; // Assuming device 0.
hipMemPool_t memPool;
hipMemPoolCreate(&memPool, &poolProps);
// Allocate memory from the pool asynchronously.
constexpr size_t numElements = 1024;
int* devData = nullptr;
hipMallocFromPoolAsync(&devData, numElements * sizeof(*devData), memPool, stream);
// Define grid and block sizes.
dim3 blockSize(256);
dim3 gridSize((numElements + blockSize.x - 1) / blockSize.x);
// Launch the kernel to perform computation.
myKernel<<<gridSize, blockSize, 0, stream>>>(devData, numElements);
// Synchronize the stream.
hipStreamSynchronize(stream);
// Copy data back to host.
int* hostData = new int[numElements];
hipMemcpy(hostData, devData, numElements * sizeof(*devData), hipMemcpyDeviceToHost);
// Print the array.
for (size_t i = 0; i < numElements; ++i) {
std::cout << "Element " << i << ": " << hostData[i] << std::endl;
}
// Free the allocated memory.
hipFreeAsync(devData, stream);
// Synchronize the stream again to ensure all operations are complete.
hipStreamSynchronize(stream);
// Destroy the memory pool and stream.
hipMemPoolDestroy(memPool);
hipStreamDestroy(stream);
// Free host memory.
delete[] hostData;
return 0;
}
Trim pools
----------
The memory allocator allows you to allocate and free memory in stream order. To control memory usage, set the release threshold attribute using ``hipMemPoolAttrReleaseThreshold``. This threshold specifies the amount of reserved memory in bytes to hold onto.
.. code-block:: cpp
uint64_t threshold = UINT64_MAX;
hipMemPoolSetAttribute(memPool, hipMemPoolAttrReleaseThreshold, &threshold);
When the amount of memory held in the memory pool exceeds the threshold, the allocator tries to release memory back to the operating system during the next call to stream, event, or context synchronization.
To improve performance, it is a good practice to adjust the memory pool size using ``hipMemPoolTrimTo()``. It helps to reclaim memory from an excessive memory pool, which optimizes memory usage for your application.
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <iostream>
int main() {
hipMemPool_t memPool;
hipDevice_t device = 0; // Specify the device index.
// Initialize the device.
hipSetDevice(device);
// Get the default memory pool for the device.
hipDeviceGetDefaultMemPool(&memPool, device);
// Allocate memory from the pool (e.g., 1 MB).
size_t allocSize = 1 * 1024 * 1024;
void* ptr;
hipMalloc(&ptr, allocSize);
// Free the allocated memory.
hipFree(ptr);
// Trim the memory pool to a specific size (e.g., 512 KB).
size_t newSize = 512 * 1024;
hipMemPoolTrimTo(memPool, newSize);
// Clean up.
hipMemPoolDestroy(memPool);
std::cout << "Memory pool trimmed to " << newSize << " bytes." << std::endl;
return 0;
}
Resource usage statistics
-------------------------
Resource usage statistics help in optimization. Here is the list of pool attributes used to query memory usage:
- ``hipMemPoolAttrReservedMemCurrent``: Returns the total physical GPU memory currently held in the pool.
- ``hipMemPoolAttrUsedMemCurrent``: Returns the total size of all the memory allocated from the pool.
- ``hipMemPoolAttrReservedMemHigh``: Returns the total physical GPU memory held in the pool since the last reset.
- ``hipMemPoolAttrUsedMemHigh``: Returns the total size of all the memory allocated from the pool since the last reset.
To reset these attributes to the current value, use ``hipMemPoolSetAttribute()``.
.. code-block:: cpp
#include <iostream>
#include <hip/hip_runtime.h>
// Sample helper functions for getting the usage statistics in bulk.
struct usageStatistics {
uint64_t reservedMemCurrent;
uint64_t reservedMemHigh;
uint64_t usedMemCurrent;
uint64_t usedMemHigh;
};
void getUsageStatistics(hipMemPool_t memPool, struct usageStatistics *statistics) {
hipMemPoolGetAttribute(memPool, hipMemPoolAttrReservedMemCurrent, &statistics->reservedMemCurrent);
hipMemPoolGetAttribute(memPool, hipMemPoolAttrReservedMemHigh, &statistics->reservedMemHigh);
hipMemPoolGetAttribute(memPool, hipMemPoolAttrUsedMemCurrent, &statistics->usedMemCurrent);
hipMemPoolGetAttribute(memPool, hipMemPoolAttrUsedMemHigh, &statistics->usedMemHigh);
}
// Resetting the watermarks resets them to the current value.
void resetStatistics(hipMemPool_t memPool) {
uint64_t value = 0;
hipMemPoolSetAttribute(memPool, hipMemPoolAttrReservedMemHigh, &value);
hipMemPoolSetAttribute(memPool, hipMemPoolAttrUsedMemHigh, &value);
}
int main() {
hipMemPool_t memPool;
hipDevice_t device = 0; // Specify the device index.
// Initialize the device.
hipSetDevice(device);
// Get the default memory pool for the device.
hipDeviceGetDefaultMemPool(&memPool, device);
// Allocate memory from the pool (e.g., 1 MB).
size_t allocSize = 1 * 1024 * 1024;
void* ptr;
hipMalloc(&ptr, allocSize);
// Free the allocated memory.
hipFree(ptr);
// Trim the memory pool to a specific size (e.g., 512 KB).
size_t newSize = 512 * 1024;
hipMemPoolTrimTo(memPool, newSize);
// Get and print usage statistics before resetting.
usageStatistics statsBefore;
getUsageStatistics(memPool, &statsBefore);
std::cout << "Before resetting statistics:" << std::endl;
std::cout << "Reserved Memory Current: " << statsBefore.reservedMemCurrent << " bytes" << std::endl;
std::cout << "Reserved Memory High: " << statsBefore.reservedMemHigh << " bytes" << std::endl;
std::cout << "Used Memory Current: " << statsBefore.usedMemCurrent << " bytes" << std::endl;
std::cout << "Used Memory High: " << statsBefore.usedMemHigh << " bytes" << std::endl;
// Reset the statistics.
resetStatistics(memPool);
// Get and print usage statistics after resetting.
usageStatistics statsAfter;
getUsageStatistics(memPool, &statsAfter);
std::cout << "After resetting statistics:" << std::endl;
std::cout << "Reserved Memory Current: " << statsAfter.reservedMemCurrent << " bytes" << std::endl;
std::cout << "Reserved Memory High: " << statsAfter.reservedMemHigh << " bytes" << std::endl;
std::cout << "Used Memory Current: " << statsAfter.usedMemCurrent << " bytes" << std::endl;
std::cout << "Used Memory High: " << statsAfter.usedMemHigh << " bytes" << std::endl;
// Clean up.
hipMemPoolDestroy(memPool);
return 0;
}
Memory reuse policies
---------------------
The allocator might reallocate memory as long as the compliant memory accesses will not to overlap temporally. To optimize the memory usage, disable or enable the following memory pool reuse policy attribute flags:
- ``hipMemPoolReuseFollowEventDependencies``: Checks event dependencies before allocating additional GPU memory.
- ``hipMemPoolReuseAllowOpportunistic``: Checks freed allocations to determine if the stream order semantic indicated by the free operation has been met.
- ``hipMemPoolReuseAllowInternalDependencies``: Manages reuse based on internal dependencies in runtime. If the driver fails to allocate and map additional physical memory, it searches for memory waiting for another stream's progress and reuses it.
Device accessibility for multi-GPU support
------------------------------------------
Allocations are initially accessible from the device where they reside.
Interprocess memory handling
=============================
Interprocess capable (IPC) memory pools facilitate efficient and secure sharing of GPU memory between processes.
To achieve interprocess memory sharing, you can use either :ref:`device pointer <device-pointer>` or :ref:`shareable handle <shareable-handle>`. Both provide allocator (export) and consumer (import) interfaces.
.. _device-pointer:
Device pointer
--------------
To export data to share a memory pool pointer directly between processes, use ``hipMemPoolExportPointer()``. It allows you to share a memory allocation with another process.
.. code-block:: cpp
#include <iostream>
#include <fstream>
#include <hip/hip_runtime.h>
#include <sys/stat.h>
int main() {
// Allocate memory.
void* devPtr;
hipMalloc(&devPtr, sizeof(int));
// Export the memory pool pointer.
hipMemPoolPtrExportData exportData;
hipError_t result = hipMemPoolExportPointer(&exportData, devPtr);
if (result != hipSuccess) {
std::cerr << "Error exporting memory pool pointer: " << hipGetErrorString(result) << std::endl;
return 1;
}
// Create a named pipe (FIFO).
const char* fifoPath = "/tmp/myfifo"; // Change this to a unique path.
mkfifo(fifoPath, 0666);
// Write the exported data to the named pipe.
std::ofstream fifoStream(fifoPath, std::ios::out | std::ios::binary);
fifoStream.write(reinterpret_cast<char*>(&exportData), sizeof(hipMemPoolPtrExportData));
fifoStream.close();
// Clean up.
hipFree(devPtr);
return 0;
}
To import a memory pool pointer directly from another process, use ``hipMemPoolImportPointer()``.
Here is how to read the pool exported in the preceding example:
.. code-block:: cpp
#include <iostream>
#include <fstream>
#include <hip/hip_runtime.h>
int main() {
// Considering that you have exported the memory pool pointer already.
// Now, let's simulate reading the exported data from a named pipe (FIFO).
const char* fifoPath = "/tmp/myfifo"; // Change this to a unique path.
std::ifstream fifoStream(fifoPath, std::ios::in | std::ios::binary);
if (!fifoStream.is_open()) {
std::cerr << "Error opening FIFO file: " << fifoPath << std::endl;
return 1;
}
// Read the exported data.
hipMemPoolPtrExportData importData;
fifoStream.read(reinterpret_cast<char*>(&importData), sizeof(hipMemPoolPtrExportData));
fifoStream.close();
if (fifoStream.fail()) {
std::cerr << "Error reading from FIFO file." << std::endl;
return 1;
}
// Create a memory pool with default properties.
hipMemPoolProps poolProps = {};
poolProps.allocType = hipMemAllocationTypePinned;
poolProps.handleTypes = hipMemHandleTypePosixFileDescriptor;
poolProps.location.type = hipMemLocationTypeDevice;
poolProps.location.id = 0; // Assuming device 0.
hipMemPool_t memPool;
hipMemPoolCreate(&memPool, &poolProps);
// Import the memory pool pointer.
void* importedDevPtr;
hipError_t result = hipMemPoolImportPointer(&importedDevPtr, memPool, &importData);
if (result != hipSuccess) {
std::cerr << "Error imported memory pool pointer: " << hipGetErrorString(result) << std::endl;
return 1;
}
// Now you can use the importedDevPtr for your computations.
// Clean up (free the memory).
hipFree(importedDevPtr);
return 0;
}
.. _shareable-handle:
Shareable handle
----------------
To export a memory pool pointer to a shareable handle, use ``hipMemPoolExportToSharedHandle()``. This handle could be a file descriptor or a handle obtained from another process. The exported handle contains information about the memory pool, such as size, location, and other relevant details.
.. code-block:: cpp
#include <iostream>
#include <fstream>
#include <hip/hip_runtime.h>
#include <sys/stat.h>
int main() {
// Create a memory pool with default properties.
hipMemPoolProps poolProps = {};
poolProps.allocType = hipMemAllocationTypePinned;
poolProps.handleTypes = hipMemHandleTypePosixFileDescriptor;
poolProps.location.type = hipMemLocationTypeDevice;
poolProps.location.id = 0; // Assuming device 0.
hipMemPool_t memPool;
hipError_t poolResult = hipMemPoolCreate(&memPool, &poolProps);
if (poolResult != hipSuccess) {
std::cerr << "Error creating memory pool: " << hipGetErrorString(poolResult) << std::endl;
return 1;
}
// Allocate memory from the memory pool.
void* devPtr;
hipMallocFromPoolAsync(&devPtr, sizeof(int), memPool, 0);
// Export the memory pool pointer.
int descriptor;
hipError_t result = hipMemPoolExportToShareableHandle(&descriptor, memPool, hipMemHandleTypePosixFileDescriptor, 0);
if (result != hipSuccess) {
std::cerr << "Error exporting memory pool pointer: " << hipGetErrorString(result) << std::endl;
return 1;
}
// Create a named pipe (FIFO).
const char* fifoPath = "/tmp/myfifo"; // Change this to a unique path.
mkfifo(fifoPath, 0666);
// Write the exported data to the named pipe.
std::ofstream fifoStream(fifoPath, std::ios::out | std::ios::binary);
fifoStream.write(reinterpret_cast<char*>(&descriptor), sizeof(int));
fifoStream.close();
// Clean up.
hipFree(devPtr);
hipMemPoolDestroy(memPool);
return 0;
}
To import and restore a memory pool pointer from a shareable handle, which could be a file descriptor or a handle obtained from another process, use ``hipMemPoolImportFromShareableHandle()``. The exported shareable handle data contains information about the memory pool, including its size, location, and other relevant details. Importing the handle provides a valid memory pointer to the same memory, which allows you to share memory across different contexts.
.. code-block:: cpp
#include <iostream>
#include <fstream>
#include <hip/hip_runtime.h>
int main() {
// Considering that you have exported the memory pool pointer already.
// Now, let's simulate reading the exported data from a named pipe (FIFO).
const char* fifoPath = "/tmp/myfifo"; // Change this to a unique path
std::ifstream fifoStream(fifoPath, std::ios::in | std::ios::binary);
if (!fifoStream.is_open()) {
std::cerr << "Error opening FIFO file: " << fifoPath << std::endl;
return 1;
}
// Read the exported data.
int descriptor;
fifoStream.read(reinterpret_cast<char*>(&descriptor), sizeof(int));
fifoStream.close();
if (fifoStream.fail()) {
std::cerr << "Error reading from FIFO file." << std::endl;
return 1;
}
// Import the memory pool.
hipMemPool_t memPool;
hipError_t result = hipMemPoolImportFromShareableHandle(&memPool, &descriptor, hipMemHandleTypePosixFileDescriptor, 0);
if (result != hipSuccess) {
std::cerr << "Error importing memory pool: " << hipGetErrorString(result) << std::endl;
return 1;
}
// Allocate memory from the imported memory pool.
void* importedDevPtr;
hipMallocFromPoolAsync(&importedDevPtr, sizeof(int), memPool, 0);
// Now you can use the importedDevPtr for your computations.
// Clean up (free the memory).
hipFree(importedDevPtr);
hipMemPoolDestroy(memPool);
return 0;
}
+740
Zobrazit soubor
@@ -0,0 +1,740 @@
.. meta::
:description: This chapter describes Unified Memory and shows
how to use it in AMD HIP.
:keywords: AMD, ROCm, HIP, CUDA, unified memory, unified, memory
.. _unified_memory:
*******************************************************************************
Unified memory management
*******************************************************************************
In conventional architectures CPUs and attached devices have their own memory
space and dedicated physical memory backing it up, e.g. normal RAM for CPUs and
VRAM on GPUs. This way each device can have physical memory optimized for its
use case. GPUs usually have specialized memory whose bandwidth is a
magnitude higher than the RAM attached to CPUs.
While providing exceptional performance, this setup typically requires explicit
memory management, as memory needs to be allocated, copied and freed on the used
devices and on the host. Additionally, this makes using more than the physically
available memory on the devices complicated.
Modern GPUs circumvent the problem of having to explicitly manage the memory,
while still keeping the benefits of the dedicated physical memories, by
supporting the concept of unified memory. This enables the CPU and the GPUs in
the system to access host and other GPUs' memory without explicit memory
management.
Unified memory
================================================================================
Unified Memory is a single memory address space accessible from any processor
within a system. This setup simplifies memory management and enables
applications to allocate data that can be read or written on both CPUs and GPUs
without explicitly copying it to the specific CPU or GPU. The Unified memory
model is shown in the following figure.
.. figure:: ../../../data/how-to/hip_runtime_api/memory_management/unified_memory/um.svg
Unified memory enables the access to memory located on other devices via
several methods, depending on whether hardware support is available or has to be
managed by the driver.
Hardware supported on-demand page migration
--------------------------------------------------------------------------------
When a kernel on the device tries to access a memory address that is not in its
memory, a page-fault is triggered. The GPU then in turn requests the page from
the host or an other device, on which the memory is located. The page is then
unmapped from the source, sent to the device and mapped to the device's memory.
The requested memory is then available to the processes running on the device.
In case the device's memory is at capacity, a page is unmapped from the device's
memory first and sent and mapped to host memory. This enables more memory to be
allocated and used for a GPU, than the GPU itself has physically available.
This level of unified memory support can be very beneficial for sparse accesses
to an array, that is not often used on the device.
Driver managed page migration
--------------------------------------------------------------------------------
If the hardware does not support on-demand page migration, then all the pages
accessed by a kernel have to be resident on the device, so they have to be
migrated before the kernel is running. Since the driver can not know beforehand,
what parts of an array are going to be accessed, all pages of all accessed
arrays have to be migrated. This can lead to significant delays on the first run
of a kernel, on top of possibly copying more memory than is actually accessed by
the kernel.
.. _unified memory system requirements:
System requirements
================================================================================
Unified memory is supported on Linux by all modern AMD GPUs from the Vega
series onward, as shown in the following table. Unified memory management can
be achieved by explicitly allocating managed memory using
:cpp:func:`hipMallocManaged` or marking variables with the ``__managed__``
attribute. For the latest GPUs, with a Linux kernel that supports
`Heterogeneous Memory Management (HMM)
<https://www.kernel.org/doc/html/latest/mm/hmm.html>`_, the normal system
allocator can be used.
.. list-table:: Supported Unified Memory Allocators by GPU architecture
:widths: 40, 25, 25
:header-rows: 1
:align: center
* - Architecture
- :cpp:func:`hipMallocManaged()`, ``__managed__``
- ``new``, ``malloc()``
* - CDNA3
- ✅
- ✅ :sup:`1`
* - CDNA2
- ✅
- ✅ :sup:`1`
* - CDNA1
- ✅
- ✅ :sup:`1`
* - RDNA1
- ✅
- ❌
* - GCN5
- ✅
- ❌
✅: **Supported**
❌: **Unsupported**
:sup:`1` Works only with ``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>`_.
.. _unified memory allocators:
Unified memory allocators
================================================================================
Support for the different unified memory allocators depends on the GPU
architecture and on the system. For more information, see :ref:`unified memory
system requirements` and :ref:`checking unified memory support`.
- **HIP allocated managed memory and variables**
:cpp:func:`hipMallocManaged()` is a dynamic memory allocator available on
all GPUs with unified memory support. For more details, visit
:ref:`unified_memory_reference`.
The ``__managed__`` declaration specifier, which serves as its counterpart,
can be utilized for static allocation.
- **System allocated unified memory**
Starting with CDNA2, the ``new`` and ``malloc()`` system allocators allow
you to reserve unified memory. The system allocator is more versatile and
offers an easy transition for code written for CPUs to HIP code as the
same system allocation API is used.
To ensure the proper functioning of system allocated unified memory on supported
GPUs, it is essential to configure the environment variable ``XNACK=1`` and use
a kernel 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>`_.
The table below illustrates the expected behavior of managed and unified memory
functions on ROCm and CUDA, both with and without HMM support.
.. tab-set::
.. tab-item:: ROCm allocation behaviour
:sync: original-block
.. list-table:: Comparison of expected behavior of managed and unified memory functions in ROCm
:widths: 26, 17, 20, 17, 20
:header-rows: 1
* - call
- Allocation origin without HMM or ``XNACK=0``
- Access outside the origin without HMM or ``XNACK=0``
- Allocation origin with HMM and ``XNACK=1``
- Access outside the origin with HMM and ``XNACK=1``
* - ``new``, ``malloc()``
- host
- not accessible on device
- host
- page-fault migration
* - :cpp:func:`hipMalloc()`
- device
- zero copy [zc]_
- device
- zero copy [zc]_
* - :cpp:func:`hipMallocManaged()`, ``__managed__``
- pinned host
- zero copy [zc]_
- host
- page-fault migration
* - :cpp:func:`hipHostRegister()`
- undefined behavior
- undefined behavior
- host
- page-fault migration
* - :cpp:func:`hipHostMalloc()`
- pinned host
- zero copy [zc]_
- pinned host
- zero copy [zc]_
.. tab-item:: CUDA allocation behaviour
:sync: cooperative-groups
.. list-table:: Comparison of expected behavior of managed and unified memory functions in CUDA
:widths: 26, 17, 20, 17, 20
:header-rows: 1
* - call
- Allocation origin without HMM
- Access outside the origin without HMM
- Allocation origin with HMM
- Access outside the origin with HMM
* - ``new``, ``malloc()``
- host
- not accessible on device
- first touch
- page-fault migration
* - ``cudaMalloc()``
- device
- not accessible on host
- device
- page-fault migration
* - ``cudaMallocManaged()``, ``__managed__``
- host
- page-fault migration
- first touch
- page-fault migration
* - ``cudaHostRegister()``
- host
- page-fault migration
- host
- page-fault migration
* - ``cudaMallocHost()``
- pinned host
- zero copy [zc]_
- pinned host
- zero copy [zc]_
.. [zc] Zero copy is a feature, where the memory is pinned to either the device
or the host, and won't be transferred when accessed by another device or
the host. Instead only the requested memory is transferred, without
making an explicit copy, like a normal memory access, hence the term
"zero copy".
.. _checking unified memory support:
Checking unified memory support
--------------------------------------------------------------------------------
The following device attributes can offer information about which :ref:`unified
memory allocators` are supported. The attribute value is 1 if the functionality
is supported, and 0 if it is not supported.
.. list-table:: Device attributes for unified memory management
:widths: 40, 60
:header-rows: 1
:align: center
* - Attribute
- Description
* - :cpp:enumerator:`hipDeviceAttributeManagedMemory`
- Device supports allocating managed memory on this system
* - :cpp:enumerator:`hipDeviceAttributePageableMemoryAccess`
- Device supports coherently accessing pageable memory without calling :cpp:func:`hipHostRegister()` on it.
* - :cpp:enumerator:`hipDeviceAttributeConcurrentManagedAccess`
- Full unified memory support. Device can coherently access managed memory concurrently with the CPU
For details on how to get the attributes of a specific device see :cpp:func:`hipDeviceGetAttribute()`.
Example for unified memory management
--------------------------------------------------------------------------------
The following example shows how to use unified memory with
:cpp:func:`hipMallocManaged()` for dynamic allocation, the ``__managed__`` attribute
for static allocation and the standard ``new`` allocation. For comparison, the
explicit memory management example is presented in the last tab.
.. tab-set::
.. tab-item:: hipMallocManaged()
.. code-block:: cpp
:emphasize-lines: 22-25
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
int main() {
int *a, *b, *c;
// Allocate memory for a, b and c that is accessible to both device and host codes.
HIP_CHECK(hipMallocManaged(&a, sizeof(*a)));
HIP_CHECK(hipMallocManaged(&b, sizeof(*b)));
HIP_CHECK(hipMallocManaged(&c, sizeof(*c)));
// Setup input values.
*a = 1;
*b = 2;
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, a, b, c);
// Wait for GPU to finish before accessing on host.
HIP_CHECK(hipDeviceSynchronize());
// Print the result.
std::cout << *a << " + " << *b << " = " << *c << std::endl;
// Cleanup allocated memory.
HIP_CHECK(hipFree(a));
HIP_CHECK(hipFree(b));
HIP_CHECK(hipFree(c));
return 0;
}
.. tab-item:: __managed__
.. code-block:: cpp
:emphasize-lines: 19-20
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
// Declare a, b and c as static variables.
__managed__ int a, b, c;
int main() {
// Setup input values.
a = 1;
b = 2;
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, &a, &b, &c);
// Wait for GPU to finish before accessing on host.
HIP_CHECK(hipDeviceSynchronize());
// Prints the result.
std::cout << a << " + " << b << " = " << c << std::endl;
return 0;
}
.. tab-item:: new
.. code-block:: cpp
:emphasize-lines: 20-23
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int* a, int* b, int* c) {
*c = *a + *b;
}
// This example requires HMM support and the environment variable HSA_XNACK needs to be set to 1
int main() {
// Allocate memory for a, b, and c.
int *a = new int[1];
int *b = new int[1];
int *c = new int[1];
// Setup input values.
*a = 1;
*b = 2;
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, a, b, c);
// Wait for GPU to finish before accessing on host.
HIP_CHECK(hipDeviceSynchronize());
// Prints the result.
std::cout << *a << " + " << *b << " = " << *c << std::endl;
// Cleanup allocated memory.
delete[] a;
delete[] b;
delete[] c;
return 0;
}
.. tab-item:: Explicit Memory Management
.. code-block:: cpp
:emphasize-lines: 27-34, 39-40
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
int main() {
int a, b, c;
int *d_a, *d_b, *d_c;
// Setup input values.
a = 1;
b = 2;
// Allocate device copies of a, b and c.
HIP_CHECK(hipMalloc(&d_a, sizeof(*d_a)));
HIP_CHECK(hipMalloc(&d_b, sizeof(*d_b)));
HIP_CHECK(hipMalloc(&d_c, sizeof(*d_c)));
// Copy input values to device.
HIP_CHECK(hipMemcpy(d_a, &a, sizeof(*d_a), hipMemcpyHostToDevice));
HIP_CHECK(hipMemcpy(d_b, &b, sizeof(*d_b), hipMemcpyHostToDevice));
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, d_a, d_b, d_c);
// Copy the result back to the host.
HIP_CHECK(hipMemcpy(&c, d_c, sizeof(*d_c), hipMemcpyDeviceToHost));
// Cleanup allocated memory.
HIP_CHECK(hipFree(d_a));
HIP_CHECK(hipFree(d_b));
HIP_CHECK(hipFree(d_c));
// Prints the result.
std::cout << a << " + " << b << " = " << c << std::endl;
return 0;
}
.. _using unified memory:
Using unified memory
================================================================================
Unified memory can simplify the complexities of memory management in GPU
computing, by not requiring explicit copies between the host and the devices. It
can be particularly useful in use cases with sparse memory accesses from both
the CPU and the GPU, as only the parts of the memory region that are actually
accessed need to be transferred to the corresponding processor, not the whole
memory region. This reduces the amount of memory sent over the PCIe bus or other
interfaces.
In HIP, pinned memory allocations are coherent by default. Pinned memory is
host memory mapped into the address space of all GPUs, meaning that the pointer
can be used on both host and device. Additionally, using pinned memory instead of
pageable memory on the host can improve bandwidth for transfers between the host
and the GPUs.
While unified memory can provide numerous benefits, it's important to be aware
of the potential performance overhead associated with unified memory. You must
thoroughly test and profile your code to ensure it's the most suitable choice
for your use case.
.. _unified memory runtime hints:
Performance optimizations for unified memory
================================================================================
There are several ways, in which the developer can guide the runtime to reduce
copies between devices, in order to improve performance.
Data prefetching
--------------------------------------------------------------------------------
Data prefetching is a technique used to improve the performance of your
application by moving data to the desired device before it's actually
needed. ``hipCpuDeviceId`` is a special constant to specify the CPU as target.
.. code-block:: cpp
:emphasize-lines: 33-36,41-42
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
int main() {
int *a, *b, *c;
int deviceId;
HIP_CHECK(hipGetDevice(&deviceId)); // Get the current device ID
// Allocate memory for a, b and c that is accessible to both device and host codes.
HIP_CHECK(hipMallocManaged(&a, sizeof(*a)));
HIP_CHECK(hipMallocManaged(&b, sizeof(*b)));
HIP_CHECK(hipMallocManaged(&c, sizeof(*c)));
// Setup input values.
*a = 1;
*b = 2;
// Prefetch the data to the GPU device.
HIP_CHECK(hipMemPrefetchAsync(a, sizeof(*a), deviceId, 0));
HIP_CHECK(hipMemPrefetchAsync(b, sizeof(*b), deviceId, 0));
HIP_CHECK(hipMemPrefetchAsync(c, sizeof(*c), deviceId, 0));
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, a, b, c);
// Prefetch the result back to the CPU.
HIP_CHECK(hipMemPrefetchAsync(c, sizeof(*c), hipCpuDeviceId, 0));
// Wait for the prefetch operations to complete.
HIP_CHECK(hipDeviceSynchronize());
// Prints the result.
std::cout << *a << " + " << *b << " = " << *c << std::endl;
// Cleanup allocated memory.
HIP_CHECK(hipFree(a));
HIP_CHECK(hipFree(b));
HIP_CHECK(hipFree(c));
return 0;
}
Memory advice
--------------------------------------------------------------------------------
Unified memory runtime hints can be set with :cpp:func:`hipMemAdvise()` to help
improve the performance of your code if you know the memory usage pattern. There
are several different types of hints as specified in the enum
:cpp:enum:`hipMemoryAdvise`, for example, whether a certain device mostly reads
the memory region, where it should ideally be located, and even whether that
specific memory region is accessed by a specific device.
For the best performance, profile your application to optimize the
utilization of HIP runtime hints.
The effectiveness of :cpp:func:`hipMemAdvise()` comes from its ability to inform
the runtime of the developer's intentions regarding memory usage. When the
runtime has knowledge of the expected memory access patterns, it can make better
decisions about data placement, leading to less transfers via the interconnect
and thereby reduced latency and bandwidth requirements. However, the actual
impact on performance can vary based on the specific use case and the system.
The following is the updated version of the example above with memory advice
instead of prefetching.
.. code-block:: cpp
:emphasize-lines: 29-41
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
int main() {
int deviceId;
HIP_CHECK(hipGetDevice(&deviceId));
int *a, *b, *c;
// Allocate memory for a, b, and c accessible to both device and host codes.
HIP_CHECK(hipMallocManaged(&a, sizeof(*a)));
HIP_CHECK(hipMallocManaged(&b, sizeof(*b)));
HIP_CHECK(hipMallocManaged(&c, sizeof(*c)));
// Set memory advice for a and b to be read, located on and accessed by the GPU.
HIP_CHECK(hipMemAdvise(a, sizeof(*a), hipMemAdviseSetPreferredLocation, deviceId));
HIP_CHECK(hipMemAdvise(a, sizeof(*a), hipMemAdviseSetAccessedBy, deviceId));
HIP_CHECK(hipMemAdvise(a, sizeof(*a), hipMemAdviseSetReadMostly, deviceId));
HIP_CHECK(hipMemAdvise(b, sizeof(*b), hipMemAdviseSetPreferredLocation, deviceId));
HIP_CHECK(hipMemAdvise(b, sizeof(*b), hipMemAdviseSetAccessedBy, deviceId));
HIP_CHECK(hipMemAdvise(b, sizeof(*b), hipMemAdviseSetReadMostly, deviceId));
// Set memory advice for c to be read, located on and accessed by the CPU.
HIP_CHECK(hipMemAdvise(c, sizeof(*c), hipMemAdviseSetPreferredLocation, hipCpuDeviceId));
HIP_CHECK(hipMemAdvise(c, sizeof(*c), hipMemAdviseSetAccessedBy, hipCpuDeviceId));
HIP_CHECK(hipMemAdvise(c, sizeof(*c), hipMemAdviseSetReadMostly, hipCpuDeviceId));
// Setup input values.
*a = 1;
*b = 2;
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, a, b, c);
// Wait for GPU to finish before accessing on host.
HIP_CHECK(hipDeviceSynchronize());
// Prints the result.
std::cout << *a << " + " << *b << " = " << *c << std::endl;
// Cleanup allocated memory.
HIP_CHECK(hipFree(a));
HIP_CHECK(hipFree(b));
HIP_CHECK(hipFree(c));
return 0;
}
Memory range attributes
--------------------------------------------------------------------------------
:cpp:func:`hipMemRangeGetAttribute()` allows you to query attributes of a given
memory range. The attributes are given in :cpp:enum:`hipMemRangeAttribute`.
.. code-block:: cpp
:emphasize-lines: 44-49
#include <hip/hip_runtime.h>
#include <iostream>
#define HIP_CHECK(expression) \
{ \
const hipError_t err = expression; \
if(err != hipSuccess){ \
std::cerr << "HIP error: " \
<< hipGetErrorString(err) \
<< " at " << __LINE__ << "\n"; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c) {
*c = *a + *b;
}
int main() {
int *a, *b, *c;
unsigned int attributeValue;
constexpr size_t attributeSize = sizeof(attributeValue);
int deviceId;
HIP_CHECK(hipGetDevice(&deviceId));
// Allocate memory for a, b and c that is accessible to both device and host codes.
HIP_CHECK(hipMallocManaged(&a, sizeof(*a)));
HIP_CHECK(hipMallocManaged(&b, sizeof(*b)));
HIP_CHECK(hipMallocManaged(&c, sizeof(*c)));
// Setup input values.
*a = 1;
*b = 2;
HIP_CHECK(hipMemAdvise(a, sizeof(*a), hipMemAdviseSetReadMostly, deviceId));
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(1), dim3(1), 0, 0, a, b, c);
// Wait for GPU to finish before accessing on host.
HIP_CHECK(hipDeviceSynchronize());
// Query an attribute of the memory range.
HIP_CHECK(hipMemRangeGetAttribute(&attributeValue,
attributeSize,
hipMemRangeAttributeReadMostly,
a,
sizeof(*a)));
// Prints the result.
std::cout << *a << " + " << *b << " = " << *c << std::endl;
std::cout << "The array a is" << (attributeValue == 1 ? "" : " NOT") << " set to hipMemRangeAttributeReadMostly" << std::endl;
// Cleanup allocated memory.
HIP_CHECK(hipFree(a));
HIP_CHECK(hipFree(b));
HIP_CHECK(hipFree(c));
return 0;
}
Asynchronously attach memory to a stream
--------------------------------------------------------------------------------
The :cpp:func:`hipStreamAttachMemAsync()` function attaches memory to a stream,
which can reduce the amount of memory transferred, when managed memory is used.
When the memory is attached to a stream using this function, it only gets
transferred between devices, when a kernel that is launched on this stream needs
access to the memory.
+154
Zobrazit soubor
@@ -0,0 +1,154 @@
.. meta::
:description: This chapter describes introduces Virtual Memory (VM) and shows
how to use it in AMD HIP.
:keywords: AMD, ROCm, HIP, CUDA, virtual memory, virtual, memory, UM, APU
.. _virtual_memory:
********************************************************************************
Virtual memory management
********************************************************************************
Memory management is important when creating high-performance applications in
the HIP ecosystem. Both allocating and copying memory can result in bottlenecks,
which can significantly impact performance.
Global memory allocation in HIP uses the C language style allocation function.
This works fine for simple cases but can cause problems if your memory needs
change. If you need to increase the size of your memory, you must allocate a
second larger buffer and copy the data to it before you can free the original
buffer. This increases overall memory usage and causes unnecessary ``memcpy``
calls. Another solution is to allocate a larger buffer than you initially need.
However, this isn't an efficient way to handle resources and doesn't solve the
issue of reallocation when the extra buffer runs out.
Virtual memory management solves these memory management problems. It helps to
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
is separated into multiple steps using the :cpp:func:`hipMemCreate`,
:cpp:func:`hipMemAddressReserve`, :cpp:func:`hipMemMap`, and
:cpp:func:`hipMemSetAccess` functions. This guide explains what these functions
do and how you can use them for virtual memory management.
Allocate physical memory
--------------------------------------------------------------------------------
The first step is to allocate the physical memory itself with 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`
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.
.. 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;
prop.location.id = currentDev;
hipMemGetAllocationGranularity(&granularity, &prop, HIP_MEM_ALLOC_GRANULARITY_MINIMUM);
padded_size = ROUND_UP(size, granularity);
hipMemCreate(&allocHandle, padded_size, &prop, 0);
Reserve virtual address range
--------------------------------------------------------------------------------
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.
.. code-block:: cpp
hipMemAddressReserve(&ptr, padded_size, 0, 0, 0);
hipMemMap(ptr, padded_size, 0, allocHandle, 0);
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
:cpp:struct:`hipMemAccessDesc` descriptor as parameters. In a multi-GPU
environment, you can map the device memory of one GPU to another. This feature
also works with the traditional memory management system, but isn't as scalable
as with virtual memory. When memory is allocated with :cpp:func:`hipMalloc`,
:cpp:func:`hipDeviceEnablePeerAccess` is used to enable peer access. This
function enables access between two devices, but it means that every call to
:cpp:func:`hipMalloc` takes more time to perform the checks and the mapping
between the devices. When using virtual memory management, peer access is
enabled by :cpp:func:`hipMemSetAccess`, which provides a finer level of
control over what is shared. This has no performance impact on memory allocation
and gives you more control over what memory buffers are shared with which
devices.
.. code-block:: cpp
hipMemAccessDesc accessDesc = {};
accessDesc.location.type = HIP_MEM_LOCATION_TYPE_DEVICE;
accessDesc.location.id = currentDev;
accessDesc.flags = HIP_MEM_ACCESS_FLAGS_PROT_READWRITE;
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.
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
virtual address range, use :cpp:func:`hipMemAddressFree`. Finally, to release
the physical memory, use :cpp:func:`hipMemRelease`. A side effect of these
functions is the lack of synchronization when memory is released. If you call
:cpp:func:`hipFree` when you have multiple streams running in parallel, it
synchronizes the device. This causes worse resource usage and performance.
.. code-block:: cpp
hipMemUnmap(ptr, size);
hipMemRelease(allocHandle);
hipMemAddressFree(ptr, size);
.. _usage_virtual_memory:
Memory usage
================================================================================
Dynamically increase allocation size
--------------------------------------------------------------------------------
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.
.. 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.