Sync HIP documentation 2025-10-20 (#1258)

* Add examples to tools folder
* Correct P2P memory access section
* Sync poriting guide
* Add HIP Graph tutorial
* Add hint about using amdgpu-dkms for IPC API
* Add a few more env variables
This commit is contained in:
Istvan Kiss
2025-10-29 07:42:06 +01:00
committed by GitHub
parent 8e98b80deb
commit 197f73dac9
89 changed files with 10327 additions and 3486 deletions
@@ -207,319 +207,24 @@ The example codes
.. 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;
}
.. literalinclude:: ../../tools/example_codes/sequential_kernel_execution.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. 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;
}
.. literalinclude:: ../../tools/example_codes/async_kernel_execution.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. 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;
}
.. literalinclude:: ../../tools/example_codes/event_based_synchronization.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
HIP Graphs
===============================================================================
@@ -33,38 +33,10 @@ You can adjust the call stack size as shown in the following example, allowing
fine-tuning based on specific kernel requirements. This helps prevent stack
overflow errors by ensuring sufficient stack memory is allocated.
.. 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()
{
size_t stackSize;
HIP_CHECK(hipDeviceGetLimit(&stackSize, hipLimitStackSize));
std::cout << "Default stack size: " << stackSize << " bytes" << std::endl;
// Set a new stack size
size_t newStackSize = 1024 * 8; // 8 KiB
HIP_CHECK(hipDeviceSetLimit(hipLimitStackSize, newStackSize));
HIP_CHECK(hipDeviceGetLimit(&stackSize, hipLimitStackSize));
std::cout << "Updated stack size: " << stackSize << " bytes" << std::endl;
return 0;
}
.. literalinclude:: ../../tools/example_codes/call_stack_management.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Depending on the GPU model, at full occupancy, it can consume a significant
amount of memory. For instance, an MI300X with 304 compute units (CU) and up to
@@ -81,49 +53,7 @@ needed for the call stack due to the GPUs inherent parallelism. This can be
achieved by increasing stack size or optimizing code to reduce stack usage. To
detect stack overflow add proper error handling or use debugging tools.
.. 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; \
} \
}
__device__ unsigned long long fibonacci(unsigned long long n)
{
if (n == 0 || n == 1)
{
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
__global__ void kernel(unsigned long long n)
{
unsigned long long result = fibonacci(n);
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if (x == 0)
printf("%llu! = %llu \n", n, result);
}
int main()
{
kernel<<<1, 1>>>(10);
HIP_CHECK(hipDeviceSynchronize());
// With -O0 optimization option hit the stack limit
// kernel<<<1, 256>>>(2048);
// HIP_CHECK(hipDeviceSynchronize());
return 0;
}
.. literalinclude:: ../../tools/example_codes/device_recursion.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
@@ -68,70 +68,7 @@ Complete example
A complete example to demonstrate the error handling with a simple addition of
two values kernel:
.. 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; \
} \
}
// Addition of two values.
__global__ void add(int *a, int *b, int *c, size_t size) {
const size_t index = threadIdx.x + blockDim.x * blockIdx.x;
if(index < size) {
c[index] += a[index] + b[index];
}
}
int main() {
constexpr int numOfBlocks = 256;
constexpr int threadsPerBlock = 256;
constexpr size_t arraySize = 1U << 16;
std::vector<int> a(arraySize), b(arraySize), c(arraySize);
int *d_a, *d_b, *d_c;
// Setup input values.
std::fill(a.begin(), a.end(), 1);
std::fill(b.begin(), b.end(), 2);
// Allocate device copies of a, b and c.
HIP_CHECK(hipMalloc(&d_a, arraySize * sizeof(*d_a)));
HIP_CHECK(hipMalloc(&d_b, arraySize * sizeof(*d_b)));
HIP_CHECK(hipMalloc(&d_c, arraySize * sizeof(*d_c)));
// Copy input values to device.
HIP_CHECK(hipMemcpy(d_a, &a, arraySize * sizeof(*d_a), hipMemcpyHostToDevice));
HIP_CHECK(hipMemcpy(d_b, &b, arraySize * sizeof(*d_b), hipMemcpyHostToDevice));
// Launch add() kernel on GPU.
hipLaunchKernelGGL(add, dim3(numOfBlocks), dim3(threadsPerBlock), 0, 0, d_a, d_b, d_c, arraySize);
// Check the kernel launch
HIP_CHECK(hipGetLastError());
// Check for kernel execution error
HIP_CHECK(hipDeviceSynchronize());
// Copy the result back to the host.
HIP_CHECK(hipMemcpy(&c, d_c, arraySize * sizeof(*d_c), hipMemcpyDeviceToHost));
// Cleanup allocated memory.
HIP_CHECK(hipFree(d_a));
HIP_CHECK(hipFree(d_b));
HIP_CHECK(hipFree(d_c));
// Print the result.
std::cout << a[0] << " + " << b[0] << " = " << c[0] << std::endl;
return 0;
}
.. literalinclude:: ../../tools/example_codes/error_handling.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
@@ -14,6 +14,10 @@ method via streams. A HIP graph is made up of nodes and edges. The nodes of a
HIP graph represent the operations performed, while the edges mark dependencies
between those operations.
.. hint::
The :ref:`HIP Graph API tutorial <hip_graph_api_tutorial>` demonstrates how
to use HIP graphs in a real-world application.
The nodes can be one of the following:
- empty nodes
@@ -180,124 +184,10 @@ The general flow for using stream capture to create a graph template is:
The following code is an example of how to use the HIP graph API to capture a
graph from a stream.
.. 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; \
} \
}
__global__ void kernelA(double* arrayA, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] *= 2.0;}
};
__global__ void kernelB(int* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayB[x] = 3;}
};
__global__ void kernelC(double* arrayA, const int* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] += arrayB[x];}
};
struct set_vector_args{
std::vector<double>& h_array;
double value;
};
void set_vector(void* args){
set_vector_args h_args{*(reinterpret_cast<set_vector_args*>(args))};
std::vector<double>& vec{h_args.h_array};
vec.assign(vec.size(), h_args.value);
}
int main(){
constexpr int numOfBlocks = 1024;
constexpr int threadsPerBlock = 1024;
constexpr size_t arraySize = 1U << 20;
// This example assumes that kernelA operates on data that needs to be initialized on
// and copied from the host, while kernelB initializes the array that is passed to it.
// Both arrays are then used as input to kernelC, where arrayA is also used as
// output, that is copied back to the host, while arrayB is only read from and not modified.
double* d_arrayA;
int* d_arrayB;
std::vector<double> h_array(arraySize);
constexpr double initValue = 2.0;
hipStream_t captureStream;
HIP_CHECK(hipStreamCreate(&captureStream));
// Start capturing the operations assigned to the stream
HIP_CHECK(hipStreamBeginCapture(captureStream, hipStreamCaptureModeGlobal));
// hipMallocAsync and hipMemcpyAsync are needed, to be able to assign it to a stream
HIP_CHECK(hipMallocAsync(&d_arrayA, arraySize*sizeof(double), captureStream));
HIP_CHECK(hipMallocAsync(&d_arrayB, arraySize*sizeof(int), captureStream));
// Assign host function to the stream
// Needs a custom struct to pass the arguments
set_vector_args args{h_array, initValue};
HIP_CHECK(hipLaunchHostFunc(captureStream, set_vector, &args));
HIP_CHECK(hipMemcpyAsync(d_arrayA, h_array.data(), arraySize*sizeof(double), hipMemcpyHostToDevice, captureStream));
kernelA<<<numOfBlocks, threadsPerBlock, 0, captureStream>>>(d_arrayA, arraySize);
kernelB<<<numOfBlocks, threadsPerBlock, 0, captureStream>>>(d_arrayB, arraySize);
kernelC<<<numOfBlocks, threadsPerBlock, 0, captureStream>>>(d_arrayA, d_arrayB, arraySize);
HIP_CHECK(hipMemcpyAsync(h_array.data(), d_arrayA, arraySize*sizeof(*d_arrayA), hipMemcpyDeviceToHost, captureStream));
HIP_CHECK(hipFreeAsync(d_arrayA, captureStream));
HIP_CHECK(hipFreeAsync(d_arrayB, captureStream));
// Stop capturing
hipGraph_t graph;
HIP_CHECK(hipStreamEndCapture(captureStream, &graph));
// Create an executable graph from the captured graph
hipGraphExec_t graphExec;
HIP_CHECK(hipGraphInstantiate(&graphExec, graph, nullptr, nullptr, 0));
// The graph template can be deleted after the instantiation if it's not needed for later use
HIP_CHECK(hipGraphDestroy(graph));
// Actually launch the graph. The stream does not have
// to be the same as the one used for capturing.
HIP_CHECK(hipGraphLaunch(graphExec, captureStream));
// Verify results
constexpr double expected = initValue * 2.0 + 3;
bool passed = true;
for(size_t i = 0; i < arraySize; ++i){
if(h_array[i] != expected){
passed = false;
std::cerr << "Validation failed! Expected " << expected << " got " << h_array[0] << std::endl;
break;
}
}
if(passed){
std::cerr << "Validation passed." << std::endl;
}
// Free graph and stream resources after usage
HIP_CHECK(hipGraphExecDestroy(graphExec));
HIP_CHECK(hipStreamDestroy(captureStream));
}
.. literalinclude:: ../../tools/example_codes/graph_capture.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Explicit graph creation
================================================================================
@@ -333,178 +223,7 @@ The general flow for explicitly creating a graph is usually:
The following code example demonstrates how to explicitly create nodes in order to create a graph.
.. 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; \
} \
}
__global__ void kernelA(double* arrayA, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] *= 2.0;}
};
__global__ void kernelB(int* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayB[x] = 3;}
};
__global__ void kernelC(double* arrayA, const int* arrayB, size_t size){
const size_t x = threadIdx.x + blockDim.x * blockIdx.x;
if(x < size){arrayA[x] += arrayB[x];}
};
struct set_vector_args{
std::vector<double>& h_array;
double value;
};
void set_vector(void* args){
set_vector_args h_args{*(reinterpret_cast<set_vector_args*>(args))};
std::vector<double>& vec{h_args.h_array};
vec.assign(vec.size(), h_args.value);
}
int main(){
constexpr int numOfBlocks = 1024;
constexpr int threadsPerBlock = 1024;
size_t arraySize = 1U << 20;
// The pointers to the device memory don't need to be declared here,
// they are contained within the hipMemAllocNodeParams as the dptr member
std::vector<double> h_array(arraySize);
constexpr double initValue = 2.0;
// Create graph an empty graph
hipGraph_t graph;
HIP_CHECK(hipGraphCreate(&graph, 0));
// Parameters to allocate arrays
hipMemAllocNodeParams allocArrayAParams{};
allocArrayAParams.poolProps.allocType = hipMemAllocationTypePinned;
allocArrayAParams.poolProps.location.type = hipMemLocationTypeDevice;
allocArrayAParams.poolProps.location.id = 0; // GPU on which memory resides
allocArrayAParams.bytesize = arraySize * sizeof(double);
hipMemAllocNodeParams allocArrayBParams{};
allocArrayBParams.poolProps.allocType = hipMemAllocationTypePinned;
allocArrayBParams.poolProps.location.type = hipMemLocationTypeDevice;
allocArrayBParams.poolProps.location.id = 0; // GPU on which memory resides
allocArrayBParams.bytesize = arraySize * sizeof(int);
// Add the allocation nodes to the graph. They don't have any dependencies
hipGraphNode_t allocNodeA, allocNodeB;
HIP_CHECK(hipGraphAddMemAllocNode(&allocNodeA, graph, nullptr, 0, &allocArrayAParams));
HIP_CHECK(hipGraphAddMemAllocNode(&allocNodeB, graph, nullptr, 0, &allocArrayBParams));
// Parameters for the host function
// Needs custom struct to pass the arguments
set_vector_args args{h_array, initValue};
hipHostNodeParams hostParams{};
hostParams.fn = set_vector;
hostParams.userData = static_cast<void*>(&args);
// Add the host node that initializes the host array. It also doesn't have any dependencies
hipGraphNode_t hostNode;
HIP_CHECK(hipGraphAddHostNode(&hostNode, graph, nullptr, 0, &hostParams));
// Add memory copy node, that copies the initialized host array to the device.
// It has to wait for the host array to be initialized and the device memory to be allocated
hipGraphNode_t cpyNodeDependencies[] = {allocNodeA, hostNode};
hipGraphNode_t cpyToDevNode;
HIP_CHECK(hipGraphAddMemcpyNode1D(&cpyToDevNode, graph, cpyNodeDependencies, 1, allocArrayAParams.dptr, h_array.data(), arraySize * sizeof(double), hipMemcpyHostToDevice));
// Parameters for kernelA
hipKernelNodeParams kernelAParams;
void* kernelAArgs[] = {&allocArrayAParams.dptr, static_cast<void*>(&arraySize)};
kernelAParams.func = reinterpret_cast<void*>(kernelA);
kernelAParams.gridDim = numOfBlocks;
kernelAParams.blockDim = threadsPerBlock;
kernelAParams.sharedMemBytes = 0;
kernelAParams.kernelParams = kernelAArgs;
kernelAParams.extra = nullptr;
// Add the node for kernelA. It has to wait for the memory copy to finish, as it depends on the values from the host array.
hipGraphNode_t kernelANode;
HIP_CHECK(hipGraphAddKernelNode(&kernelANode, graph, &cpyToDevNode, 1, &kernelAParams));
// Parameters for kernelB
hipKernelNodeParams kernelBParams;
void* kernelBArgs[] = {&allocArrayBParams.dptr, static_cast<void*>(&arraySize)};
kernelBParams.func = reinterpret_cast<void*>(kernelB);
kernelBParams.gridDim = numOfBlocks;
kernelBParams.blockDim = threadsPerBlock;
kernelBParams.sharedMemBytes = 0;
kernelBParams.kernelParams = kernelBArgs;
kernelBParams.extra = nullptr;
// Add the node for kernelB. It only has to wait for the memory to be allocated, as it initializes the array.
hipGraphNode_t kernelBNode;
HIP_CHECK(hipGraphAddKernelNode(&kernelBNode, graph, &allocNodeB, 1, &kernelBParams));
// Parameters for kernelC
hipKernelNodeParams kernelCParams;
void* kernelCArgs[] = {&allocArrayAParams.dptr, &allocArrayBParams.dptr, static_cast<void*>(&arraySize)};
kernelCParams.func = reinterpret_cast<void*>(kernelC);
kernelCParams.gridDim = numOfBlocks;
kernelCParams.blockDim = threadsPerBlock;
kernelCParams.sharedMemBytes = 0;
kernelCParams.kernelParams = kernelCArgs;
kernelCParams.extra = nullptr;
// Add the node for kernelC. It has to wait on both kernelA and kernelB to finish, as it depends on their results.
hipGraphNode_t kernelCNode;
hipGraphNode_t kernelCDependencies[] = {kernelANode, kernelBNode};
HIP_CHECK(hipGraphAddKernelNode(&kernelCNode, graph, kernelCDependencies, 1, &kernelCParams));
// Copy the results back to the host. Has to wait for kernelC to finish.
hipGraphNode_t cpyToHostNode;
HIP_CHECK(hipGraphAddMemcpyNode1D(&cpyToHostNode, graph, &kernelCNode, 1, h_array.data(), allocArrayAParams.dptr, arraySize * sizeof(double), hipMemcpyDeviceToHost));
// Free array of allocNodeA. It needs to wait for the copy to finish, as kernelC stores its results in it.
hipGraphNode_t freeNodeA;
HIP_CHECK(hipGraphAddMemFreeNode(&freeNodeA, graph, &cpyToHostNode, 1, allocArrayAParams.dptr));
// Free array of allocNodeB. It only needs to wait for kernelC to finish, as it is not written back to the host.
hipGraphNode_t freeNodeB;
HIP_CHECK(hipGraphAddMemFreeNode(&freeNodeB, graph, &kernelCNode, 1, allocArrayBParams.dptr));
// Instantiate the graph in order to execute it
hipGraphExec_t graphExec;
HIP_CHECK(hipGraphInstantiate(&graphExec, graph, nullptr, nullptr, 0));
// The graph can be freed after the instantiation if it's not needed for other purposes
HIP_CHECK(hipGraphDestroy(graph));
// Actually launch the graph
hipStream_t graphStream;
HIP_CHECK(hipStreamCreate(&graphStream));
HIP_CHECK(hipGraphLaunch(graphExec, graphStream));
// Verify results
constexpr double expected = initValue * 2.0 + 3;
bool passed = true;
for(size_t i = 0; i < arraySize; ++i){
if(h_array[i] != expected){
passed = false;
std::cerr << "Validation failed! Expected " << expected << " got " << h_array[0] << std::endl;
break;
}
}
if(passed){
std::cerr << "Validation passed." << std::endl;
}
HIP_CHECK(hipGraphExecDestroy(graphExec));
HIP_CHECK(hipStreamDestroy(graphStream));
}
.. literalinclude:: ../../tools/example_codes/graph_creation.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
@@ -66,24 +66,10 @@ which can be used to loop over the available GPUs.
Example code of querying GPUs:
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <iostream>
int main() {
int deviceCount;
if (hipGetDeviceCount(&deviceCount) == hipSuccess){
for (int i = 0; i < deviceCount; ++i){
hipDeviceProp_t prop;
if ( hipGetDeviceProperties(&prop, i) == hipSuccess)
std::cout << "Device" << i << prop.name << std::endl;
}
}
return 0;
}
.. literalinclude:: ../../tools/example_codes/simple_device_query.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Setting the GPU
--------------------------------------------------------------------------------
@@ -47,61 +47,10 @@ 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));
}
.. literalinclude:: ../../../tools/example_codes/pageable_host_memory.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. note::
@@ -133,61 +82,10 @@ 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));
}
.. literalinclude:: ../../../tools/example_codes/pinned_host_memory.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. _memory_allocation_flags:
@@ -37,102 +37,17 @@ 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;
}
.. literalinclude:: ../../../tools/example_codes/stream_ordered_memory_allocation.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. 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;
}
.. literalinclude:: ../../../tools/example_codes/ordinary_memory_allocation.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
For more details, see :ref:`stream_ordered_memory_allocator_reference`.
@@ -148,121 +63,29 @@ The ``hipMallocAsync()`` function uses the current memory pool and also provides
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;
}
.. literalinclude:: ../../../tools/example_codes/memory_pool.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
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);
.. literalinclude:: ../../../tools/example_codes/memory_pool_threshold.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
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;
}
.. literalinclude:: ../../../tools/example_codes/memory_pool_trim.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Resource usage statistics
-------------------------
@@ -276,81 +99,10 @@ Resource usage statistics help in optimization. Here is the list of pool attribu
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;
}
.. literalinclude:: ../../../tools/example_codes/memory_pool_resource_usage_statistics.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Memory reuse policies
---------------------
@@ -369,6 +121,11 @@ Allocations are initially accessible from the device where they reside.
Interprocess memory handling
=============================
.. attention::
IPC API calls are only supported on systems with an active ``amdgpu-dkms`` driver. Please refer to the
`AMDGPU documentation <https://instinct.docs.amd.com/projects/amdgpu-docs/en/latest/index.html>`__ for information
on how to install ``amdgpu-dkms``.
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.
@@ -303,207 +303,35 @@ explicit memory management example is presented in the last tab.
.. tab-item:: hipMallocManaged()
.. code-block:: cpp
.. literalinclude:: ../../../tools/example_codes/dynamic_unified_memory.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
.. tab-item:: __managed__
.. code-block:: cpp
.. literalinclude:: ../../../tools/example_codes/static_unified_memory.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
.. tab-item:: new
.. code-block:: cpp
.. literalinclude:: ../../../tools/example_codes/standard_unified_memory.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:emphasize-lines: 21-24
#include <hip/hip_runtime.h>
#include <iostream>
#include <new>
#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 with proper alignment for performance
int *a = new(std::align_val_t(128)) int[1];
int *b = new(std::align_val_t(128)) int[1];
int *c = new(std::align_val_t(128)) 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 with matching aligned delete.
::operator delete[](a, std::align_val_t(128));
::operator delete[](b, std::align_val_t(128));
::operator delete[](c, std::align_val_t(128));
return 0;
}
:language: cpp
.. tab-item:: Explicit Memory Management
.. code-block:: cpp
.. literalinclude:: ../../../tools/example_codes/explicit_memory.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
.. _using unified memory:
@@ -559,65 +387,11 @@ 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
.. literalinclude:: ../../../tools/example_codes/data_prefetching.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
Memory advice
--------------------------------------------------------------------------------
@@ -642,71 +416,11 @@ 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
.. literalinclude:: ../../../tools/example_codes/unified_memory_advice.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
Memory range attributes
--------------------------------------------------------------------------------
@@ -714,70 +428,11 @@ 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
.. literalinclude:: ../../../tools/example_codes/memory_range_attributes.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
: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;
}
:language: cpp
Asynchronously attach memory to a stream
--------------------------------------------------------------------------------
@@ -22,43 +22,10 @@ dynamic selections during runtime to ensure optimal performance.
If the application does not define a specific GPU, device 0 is selected.
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <iostream>
int main()
{
int deviceCount;
hipGetDeviceCount(&deviceCount);
std::cout << "Number of devices: " << deviceCount << std::endl;
for (int deviceId = 0; deviceId < deviceCount; ++deviceId)
{
hipDeviceProp_t deviceProp;
hipGetDeviceProperties(&deviceProp, deviceId);
std::cout << "Device " << deviceId << std::endl << " Properties:" << std::endl;
std::cout << " Name: " << deviceProp.name << std::endl;
std::cout << " Total Global Memory: " << deviceProp.totalGlobalMem / (1024 * 1024) << " MiB" << std::endl;
std::cout << " Shared Memory per Block: " << deviceProp.sharedMemPerBlock / 1024 << " KiB" << std::endl;
std::cout << " Registers per Block: " << deviceProp.regsPerBlock << std::endl;
std::cout << " Warp Size: " << deviceProp.warpSize << std::endl;
std::cout << " Max Threads per Block: " << deviceProp.maxThreadsPerBlock << std::endl;
std::cout << " Max Threads per Multiprocessor: " << deviceProp.maxThreadsPerMultiProcessor << std::endl;
std::cout << " Number of Multiprocessors: " << deviceProp.multiProcessorCount << std::endl;
std::cout << " Max Threads Dimensions: ["
<< deviceProp.maxThreadsDim[0] << ", "
<< deviceProp.maxThreadsDim[1] << ", "
<< deviceProp.maxThreadsDim[2] << "]" << std::endl;
std::cout << " Max Grid Size: ["
<< deviceProp.maxGridSize[0] << ", "
<< deviceProp.maxGridSize[1] << ", "
<< deviceProp.maxGridSize[2] << "]" << std::endl;
std::cout << std::endl;
}
return 0;
}
.. literalinclude:: ../../tools/example_codes/device_enumeration.cpp
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
.. _multi_device_selection:
@@ -72,71 +39,10 @@ different GPUs might have different capabilities or workloads. By selecting the
appropriate device, you ensure that the computational tasks are directed to the
correct GPU, optimizing performance and resource utilization.
.. 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; \
exit(status); \
} \
}
__global__ void simpleKernel(double *data)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = idx * 2.0;
}
int main()
{
double* deviceData0;
double* deviceData1;
size_t size = 1024 * sizeof(*deviceData0);
int deviceId0 = 0;
int deviceId1 = 1;
// Set device 0 and perform operations
HIP_CHECK(hipSetDevice(deviceId0)); // Set device 0 as current
HIP_CHECK(hipMalloc(&deviceData0, size)); // Allocate memory on device 0
simpleKernel<<<1000, 128>>>(deviceData0); // Launch kernel on device 0
HIP_CHECK(hipDeviceSynchronize());
// Set device 1 and perform operations
HIP_CHECK(hipSetDevice(deviceId1)); // Set device 1 as current
HIP_CHECK(hipMalloc(&deviceData1, size)); // Allocate memory on device 1
simpleKernel<<<1000, 128>>>(deviceData1); // Launch kernel on device 1
HIP_CHECK(hipDeviceSynchronize());
// Copy result from device 0
double hostData0[1024];
HIP_CHECK(hipSetDevice(deviceId0));
HIP_CHECK(hipMemcpy(hostData0, deviceData0, size, hipMemcpyDeviceToHost));
// Copy result from device 1
double hostData1[1024];
HIP_CHECK(hipSetDevice(deviceId1));
HIP_CHECK(hipMemcpy(hostData1, deviceData1, size, hipMemcpyDeviceToHost));
// Display results from both devices
std::cout << "Device 0 data: " << hostData0[0] << std::endl;
std::cout << "Device 1 data: " << hostData1[0] << std::endl;
// Free device memory
HIP_CHECK(hipFree(deviceData0));
HIP_CHECK(hipFree(deviceData1));
return 0;
}
.. literalinclude:: ../../tools/example_codes/device_selection.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Stream and event behavior
===============================================================================
@@ -151,100 +57,10 @@ conditions and optimizes data flow in multi-GPU systems. Together, streams and
events maximize performance by enabling parallel execution, load balancing, and
effective resource utilization across heterogeneous hardware.
.. code-block:: cpp
#include <hip/hip_runtime.h>
#include <iostream>
__global__ void simpleKernel(double *data)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = idx * 2.0;
}
int main()
{
int numDevices;
hipGetDeviceCount(&numDevices);
if (numDevices < 2) {
std::cerr << "This example requires at least two GPUs." << std::endl;
return -1;
}
double *deviceData0, *deviceData1;
size_t size = 1024 * sizeof(*deviceData0);
// Create streams and events for each device
hipStream_t stream0, stream1;
hipEvent_t startEvent0, stopEvent0, startEvent1, stopEvent1;
// Initialize device 0
hipSetDevice(0);
hipStreamCreate(&stream0);
hipEventCreate(&startEvent0);
hipEventCreate(&stopEvent0);
hipMalloc(&deviceData0, size);
// Initialize device 1
hipSetDevice(1);
hipStreamCreate(&stream1);
hipEventCreate(&startEvent1);
hipEventCreate(&stopEvent1);
hipMalloc(&deviceData1, size);
// Record the start event on device 0
hipSetDevice(0);
hipEventRecord(startEvent0, stream0);
// Launch the kernel asynchronously on device 0
simpleKernel<<<1000, 128, 0, stream0>>>(deviceData0);
// Record the stop event on device 0
hipEventRecord(stopEvent0, stream0);
// Wait for the stop event on device 0 to complete
hipEventSynchronize(stopEvent0);
// Record the start event on device 1
hipSetDevice(1);
hipEventRecord(startEvent1, stream1);
// Launch the kernel asynchronously on device 1
simpleKernel<<<1000, 128, 0, stream1>>>(deviceData1);
// Record the stop event on device 1
hipEventRecord(stopEvent1, stream1);
// Wait for the stop event on device 1 to complete
hipEventSynchronize(stopEvent1);
// Calculate elapsed time between the events for both devices
float milliseconds0 = 0, milliseconds1 = 0;
hipEventElapsedTime(&milliseconds0, startEvent0, stopEvent0);
hipEventElapsedTime(&milliseconds1, startEvent1, stopEvent1);
std::cout << "Elapsed time on GPU 0: " << milliseconds0 << " ms" << std::endl;
std::cout << "Elapsed time on GPU 1: " << milliseconds1 << " ms" << std::endl;
// Cleanup for device 0
hipSetDevice(0);
hipEventDestroy(startEvent0);
hipEventDestroy(stopEvent0);
hipStreamSynchronize(stream0);
hipStreamDestroy(stream0);
hipFree(deviceData0);
// Cleanup for device 1
hipSetDevice(1);
hipEventDestroy(startEvent1);
hipEventDestroy(stopEvent1);
hipStreamSynchronize(stream1);
hipStreamDestroy(stream1);
hipFree(deviceData1);
return 0;
}
.. literalinclude:: ../../tools/example_codes/multi_device_synchronization.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:language: cpp
Peer-to-peer memory access
===============================================================================
@@ -257,164 +73,25 @@ applications that require frequent data exchange between GPUs, as it eliminates
the need to transfer data through the host memory.
By adding peer-to-peer access to the example referenced in
:ref:`multi_device_selection`, data can be copied between devices:
:ref:`multi_device_selection`, data can be efficiently copied between devices.
If peer-to-peer access is not activated, the call to :cpp:func:`hipMemcpy`
still works but internally uses a staging buffer in host memory, which incurs a
performance penalty.
.. tab-set::
.. tab-item:: with peer-to-peer
.. code-block:: cpp
:emphasize-lines: 31-37, 51-55
#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; \
exit(status); \
} \
}
__global__ void simpleKernel(double *data)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = idx * 2.0;
}
int main()
{
double* deviceData0;
double* deviceData1;
size_t size = 1024 * sizeof(*deviceData0);
int deviceId0 = 0;
int deviceId1 = 1;
// Enable peer access to the memory (allocated and future) on the peer device.
// Ensure the device is active before enabling peer access.
hipSetDevice(deviceId0);
hipDeviceEnablePeerAccess(deviceId1, 0);
hipSetDevice(deviceId1);
hipDeviceEnablePeerAccess(deviceId0, 0);
// Set device 0 and perform operations
HIP_CHECK(hipSetDevice(deviceId0)); // Set device 0 as current
HIP_CHECK(hipMalloc(&deviceData0, size)); // Allocate memory on device 0
simpleKernel<<<1000, 128>>>(deviceData0); // Launch kernel on device 0
HIP_CHECK(hipDeviceSynchronize());
// Set device 1 and perform operations
HIP_CHECK(hipSetDevice(deviceId1)); // Set device 1 as current
HIP_CHECK(hipMalloc(&deviceData1, size)); // Allocate memory on device 1
simpleKernel<<<1000, 128>>>(deviceData1); // Launch kernel on device 1
HIP_CHECK(hipDeviceSynchronize());
// Use peer-to-peer access
hipSetDevice(deviceId0);
// Now device 0 can access memory allocated on device 1
hipMemcpy(deviceData0, deviceData1, size, hipMemcpyDeviceToDevice);
// Copy result from device 0
double hostData0[1024];
HIP_CHECK(hipSetDevice(deviceId0));
HIP_CHECK(hipMemcpy(hostData0, deviceData0, size, hipMemcpyDeviceToHost));
// Copy result from device 1
double hostData1[1024];
HIP_CHECK(hipSetDevice(deviceId1));
HIP_CHECK(hipMemcpy(hostData1, deviceData1, size, hipMemcpyDeviceToHost));
// Display results from both devices
std::cout << "Device 0 data: " << hostData0[0] << std::endl;
std::cout << "Device 1 data: " << hostData1[0] << std::endl;
// Free device memory
HIP_CHECK(hipFree(deviceData0));
HIP_CHECK(hipFree(deviceData1));
return 0;
}
.. literalinclude:: ../../tools/example_codes/p2p_memory_access.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:emphasize-lines: 43-49, 63-67
:language: cpp
.. tab-item:: without peer-to-peer
.. code-block:: cpp
:emphasize-lines: 43-49, 53, 58
#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; \
exit(status); \
} \
}
__global__ void simpleKernel(double *data)
{
int idx = blockIdx.x * blockDim.x + threadIdx.x;
data[idx] = idx * 2.0;
}
int main()
{
double* deviceData0;
double* deviceData1;
size_t size = 1024 * sizeof(*deviceData0);
int deviceId0 = 0;
int deviceId1 = 1;
// Set device 0 and perform operations
HIP_CHECK(hipSetDevice(deviceId0)); // Set device 0 as current
HIP_CHECK(hipMalloc(&deviceData0, size)); // Allocate memory on device 0
simpleKernel<<<1000, 128>>>(deviceData0); // Launch kernel on device 0
HIP_CHECK(hipDeviceSynchronize());
// Set device 1 and perform operations
HIP_CHECK(hipSetDevice(deviceId1)); // Set device 1 as current
HIP_CHECK(hipMalloc(&deviceData1, size)); // Allocate memory on device 1
simpleKernel<<<1000, 128>>>(deviceData1); // Launch kernel on device 1
HIP_CHECK(hipDeviceSynchronize());
// Attempt to use deviceData0 on device 1 (This will not work as deviceData0 is allocated on device 0)
HIP_CHECK(hipSetDevice(deviceId1));
hipError_t err = hipMemcpy(deviceData1, deviceData0, size, hipMemcpyDeviceToDevice); // This should fail
if (err != hipSuccess)
{
std::cout << "Error: Cannot access deviceData0 from device 1, deviceData0 is on device 0" << std::endl;
}
// Copy result from device 0
double hostData0[1024];
HIP_CHECK(hipSetDevice(deviceId0));
HIP_CHECK(hipMemcpy(hostData0, deviceData0, size, hipMemcpyDeviceToHost));
// Copy result from device 1
double hostData1[1024];
HIP_CHECK(hipSetDevice(deviceId1));
HIP_CHECK(hipMemcpy(hostData1, deviceData1, size, hipMemcpyDeviceToHost));
// Display results from both devices
std::cout << "Device 0 data: " << hostData0[0] << std::endl;
std::cout << "Device 1 data: " << hostData1[0] << std::endl;
// Free device memory
HIP_CHECK(hipFree(deviceData0));
HIP_CHECK(hipFree(deviceData1));
return 0;
}
.. literalinclude:: ../../tools/example_codes/p2p_memory_access_host_staging.hip
:start-after: // [sphinx-start]
:end-before: // [sphinx-end]
:emphasize-lines: 55-57
:language: cpp