SIMT threads can access multiple types of memory spaces. The following table summarizes the scopes and lifecycles of common memory types in SIMT programming.
|
Memory Type |
Thread Scope |
Lifecycle |
Physical Location |
|---|---|---|---|
|
Global memory |
Grid |
Application |
Device |
|
Shared memory |
Block |
Kernel function |
Vector Core |
|
Stack |
Thread |
Kernel function |
Device |
|
Register |
Thread |
Kernel function |
Vector Core |
The following figure shows the memory hierarchy.

The global memory on the device can be accessed by all threads in the entire grid. Its function is similar to that of the random access memory (RAM) in the CPU system. Kernel functions running on the device can directly access the global memory, which is the same as accessing the system memory by the code on the CPU.
The global memory is persistent. Data in the space allocated from the global memory will be retained until the space is released or the application is terminated. You can use runtime APIs to manage the global memory on the device. The host uses aclrtMalloc to allocate the global memory on the device, and uses aclrtMemcpy to copy data from the host to the global memory on the device or from the global memory on the device to the host. The global memory on the device allocated by aclrtMalloc must be freed using the aclrtFree API. For more information about runtime APIs, see Runtime API Reference.
During actual development, you need to allocate and initialize the global memory using runtime APIs before starting the kernel function. During kernel function execution, each thread in the SIMT can read data from and write data to the global memory. After the kernel function is executed, the result written to the global memory can be copied back to the host. The global memory can be accessed by all threads in a grid. Therefore, data races between threads must be strictly avoided.
The following is a simple code example of using the global memory: Arrays x, y, and z are stored in the global memory. The following kernel function is used to implement the global memory access and storage of each thread:
1 2 3 4 5 6 7 8 9 10 |
__global__ void add_custom(float* x, float* y, float* z, uint64_t total_length) { // Calculate global thread ID int32_t idx = blockIdx.x * blockDim.x + threadIdx.x; // Maps to the row index of output tensor if (idx >= total_length) { return; } z[idx] = x[idx] + y[idx]; } |
The shared memory can be accessed by all threads in the same thread block. It is located inside each Vector Core (AIV). Compared with the global memory, the shared memory has a smaller capacity but higher bandwidth and lower access latency. It can be considered as a high-speed cache resource managed by the user during kernel execution. The shared memory can be accessed by all threads in a thread block. Therefore, data races between threads in the same thread block must be avoided. The asc_syncthreads API can be used to synchronize threads in the same thread block. This function blocks all threads in the thread block until all threads reach the position where the API is called.
You can allocate the shared memory dynamically or statically.
1
|
__ubuf__ half staticBuf[1024]; |
1
|
extern __ubuf__ char dynamicBuf[]; |
The Unified Buffer not only serves as shared memory, but also reserves some memory space for internal use. Therefore, when allocating shared memory, ensure that the shared memory is not exhausted. As shown in the following figure, the size of the Unified Buffer is 256 KB, which is divided into four main areas based on functions. From the low address to high address, these areas are the static memory, dynamic memory, reserved space, and data cache.

The structure is as follows:
The static memory allocation and the method of allocating the dynamic memory through dynamic arrays are under development and will be supported in later versions.
|
blockDim Size |
Number of Available Registers for Each Thread |
|---|---|
|
1025–2048 |
16 |
|
513–1024 |
32 |
|
257–512 |
64 |
|
1–256 |
127 |
As described in the preceding table, the more threads in each thread block, the fewer registers available for each thread. If the number of threads set by the user is too large and the computation complexity of each thread is high, the compiler may temporarily store data in the stack space due to insufficient registers for storing local variables. As a result, register overflow (stack spill) may occur, affecting operator performance. Therefore, you need to properly configure blockDim based on the actual operator complexity.