デバイスメモリ
CUDAのプログラムモデルは各々のメモリを持つホストとデバイスからなるシステムを仮定している。
カーネルはデバイスメモリの外から操作するので、ランタイムはホストメモリとデバイスメモリ間のデータ転送を行う関数だけでなく、デバイスメモリの確保、解放、デバイスメモリ内のコピーを行う関数も提供している。
デバイスメモリは線形メモリ(linear memory)かCUDA配列(CUDA array)のどちらかとして確保される。
CUDA配列はテクスチャフェッチのために最適化された不透明なメモリレイアウトである。
詳しくはテクスチャとサーフェスメモリー にて。
線形メモリは一つの統一されたメモリ空間にアロケートされる。
つまり、例えば2分木やリンクリストで、別々にアロケーションされたものをポインターを通してお互いに参照することが出来る。
アドレス空間の大きさはホストシステム(CPU)と、使っているGPUのcompute capabilityに依存する。
compute capability
x86_64(AMD64)
ARM64
>= 6.0 (Pascal)
up to 47bit
up to 48bit
<= 5.3 (Maxwell)
40bit
40bit
線型メモリのアドレス空間
線形メモリは一般的にcudaMalloc()
を使って確保され、cudaFree()
で解放される。
ホストメモリとデバイスメモリ間のデータ転送はcudaMemcpy()
を用いて行われることが多い。
ベクトル和のコードサンプルでは、ベクトルはホストメモリからデバイスメモリへコピーされなければならない。
/src/programming_interface/device_memory/add_2_vectors.cu #include <cassert>
#include <cuda_runtime.h>
// Device code
__global__ void VecAdd ( float * A , float * B , float * C , int N ) {
int i = blockDim . x * blockIdx . x + threadIdx . x ;
if ( i < N ) {
C [ i ] = A [ i ] + B [ i ];
}
}
int main () {
constexpr int N = 1e6 ;
constexpr size_t size = N * sizeof ( float );
// Allocate input vectors h_A and h_B in host memory
float * h_A = ( float * ) std :: malloc ( size );
float * h_B = ( float * ) std :: malloc ( size );
float * h_C = ( float * ) std :: malloc ( size );
// Initialize input vectors
for ( int i = 0 ; i < N ; ++ i ) {
h_A [ i ] = i ;
}
for ( int i = 0 ; i < N ; ++ i ) {
h_B [ i ] = 2 * i ;
}
// Allocate vectors in device memory
float * d_A ;
cudaMalloc ( & d_A , size );
float * d_B ;
cudaMalloc ( & d_B , size );
float * d_C ;
cudaMalloc ( & d_C , size );
// Copy vectors from host memory to device memory
cudaMemcpy ( d_A , h_A , size , cudaMemcpyHostToDevice );
cudaMemcpy ( d_B , h_B , size , cudaMemcpyHostToDevice );
// Invoke kernel
int threadsPerBlock = 256 ;
int blocksPerGrid = ( N + threadsPerBlock - 1 ) / threadsPerBlock ;
VecAdd <<< blocksPerGrid , threadsPerBlock >>> ( d_A , d_B , d_C , N );
// Copy result from device memory to host memory
// h_C contains the result in host memory
cudaMemcpy ( h_C , d_C , size , cudaMemcpyDeviceToHost );
// Check result
for ( int i = 0 ; i < N ; ++ i ) {
assert ( h_C [ i ] == 3 * i );
}
// Free device memory
cudaFree ( d_A );
cudaFree ( d_B );
cudaFree ( d_C );
// Free host memory
std :: free ( h_A );
std :: free ( h_B );
std :: free ( h_C );
}
線形メモリはcudaMallocPitch とcudaMalloc3D を通しても確保出来る。
これらの関数は2次元または3次元配列のメモリ確保がアライメントの要件に合うように適切にパディングされるようにしたいときにおすすめ。
詳しくはデバイスメモリへのアクセス にて。
返されたピッチ(pitch)(またはストライド(stride))は配列要素にアクセスするように使わなければならない。
以下のコードサンプルでは、column
* row
の2次元配列をアロケートし、デバイスコード内で配列上をループする方法を示す。
/src/programming_interface/device_memory/add_2_matrices.cu #include <cuda_runtime.h>
__device__ float & at ( float * M , std :: size_t pitch , int row , int column ) {
return * (( float * )(( char * ) M + row * pitch ) + column );
}
__global__ void MatrixAdd (
float * A ,
std :: size_t pitch_A ,
float * B ,
std :: size_t pitch_B ,
float * C ,
std :: size_t pitch_C ,
int row ,
int column
) {
int i = blockDim . x * blockIdx . x + threadIdx . x ;
int j = blockDim . y * blockIdx . y + threadIdx . y ;
if ( i < row and j < column ) {
at ( C , pitch_C , i , j ) = at ( A , pitch_A , i , j ) + at ( B , pitch_B , i , j );
}
}
template < class T >
void AllocateMatrix ( T ** dev_ptr , std :: size_t * pitch , int row , int column ) {
auto e = cudaMallocPitch ( dev_ptr , pitch , sizeof ( T ) * column , row );
assert ( e == cudaSuccess );
}
void FreeMatrix ( void * dev_ptr ) {
auto e = cudaFree ( dev_ptr );
assert ( e == cudaSuccess );
}
以下のコードは、要素数がwidth
* height
* depth
の3次元配列を確保するコードである。
/src/programming_interface/device_memory/add_2_tensors.cu #include <cuda_runtime.h>
__device__ float & at ( cudaPitchedPtr devPitchedPtr , int height , int x , int y , int z ) {
char * devPtr = ( char * )( devPitchedPtr . ptr );
size_t pitch = devPitchedPtr . pitch ;
size_t slicePitch = pitch * height ;
float * row = ( float * )( devPtr + z * slicePitch + y * pitch );
return row [ x ];
}
__global__ void TensorAdd (
cudaPitchedPtr A , cudaPitchedPtr B , cudaPitchedPtr C , int width , int height , int depth
) {
int i = blockDim . x * blockIdx . x + threadIdx . x ;
int j = blockDim . y * blockIdx . y + threadIdx . y ;
int k = blockDim . z * blockIdx . z + threadIdx . z ;
if ( i < width and j < height and k < depth ) {
at ( C , height , i , j , k ) = at ( A , height , i , j , k ) + at ( B , height , i , j , k );
}
}
void AllocateTensor ( cudaPitchedPtr * devPitchedPtr , int width , int height , int depth ) {
auto e = cudaMalloc3D ( devPitchedPtr , make_cudaExtent ( width * sizeof ( float ), height , depth ));
assert ( e == cudaSuccess );
}
void FreeTensor ( void * dev_ptr ) {
auto e = cudaFree ( dev_ptr );
assert ( e == cudaSuccess );
}
void CopyTensor ( cudaPitchedPtr dst , cudaPitchedPtr src , int width , int height , int depth ) {
cudaMemcpy3DParms myParms = { 0 };
myParms . srcPtr = src ;
myParms . dstPtr = dst ;
myParms . extent = make_cudaExtent ( width * sizeof ( float ), height , depth );
myParms . kind = cudaMemcpyDefault ;
リファレンスマニュアルには、cudaMalloc()
で確保された線形メモリやcudaMallocPitch()
やcudaMalloc3D()
で確保された線形メモリ、CUDA配列、グローバルまたは定数メモリ空間で宣言された変数に対して確保されたメモリ間のコピーに使われる関数が色々ある。
以下のコードは、ランタイムAPIを使った、グローバル変数へアクセスする様々な方法を示す。
/src/programming_interface/device_memory/global_memory.cu
cudaGetSymbolAddress()
はグローバルメモリ空間で宣言された変数に対して確保されたメモリを指すアドレスを取り出すために使われる。
例はUnderstanding of CUDA's cudaGetSymbolAddress にて。
確保されたメモリのサイズはcudaGetSymbolSize()
で得られる。
2024-11-06
2024-09-27
GitHub