Numba를 이용한 Cuda 프로그램

1. Cuda 정리[1]

이 절에서는 간단한 Cuda 프로그래밍 내용을 정리한다.


1.1. Memory hierarchy

Memory hierarchy
Memory hierarchy
  • Registers
    • Thread 간에도 공유되지 않는다.
    • 함수에서 선언한 변수를 저장한다.
    • 함수에서 선언한 변수의 크기가 Thread의 Register개수를 넘을 경우 Local 메모리에 위치한다.
  • Shared memory
    • __shared__를 이용하여 선언한다.
    • Local, Global memory보다 지연시간이 적다.
    • Block내의 Thread간 공유된다.
    • __syncThreads()으로 Shared memory의 Coherent를 유지한다.
    • L1 cache와 하드웨어를 공유하고 할당량은 cudaFuncSetCache함수를 이용하여 설정가능한다.
  • Local memory
    • Registers의 공간 부족으로 Cache에 위치한 변수
  • Constant memory
    • __constant__를 이용하여 선언한다.
    • Global scope 선언되어야 한다.
    • Kernel 실행 전 cudaMemcpyToSymbo로 값을 설정하여야한다.
  • Texture memory
    • Read Only
    • 2D Spatial Locality에 최적회 되어 있다.
  • Global memory
    • __device__식별자를 이용하여 선언하거나 Host에서 cudaMalloc을 이용한다.

  • CUDA Variable and Type Qualifier
Qualifier Variable Name Memory Scope Lifespan
  float var Register Thread Thread
  float var[100] Local Thread Thread
shared float var Shared Block Block
device float var Global Global Application
constant float var Constant Global Application
  • Salient Features of Device Memory
Memory On/Off Chip Cached Access Scope Lifetime
Register On n/a R/W 1 thread Thread
Local Off Yes R/W 1 thread Thread
Shared On n/a R/W All threads in block Block
Global Off R/W All threads + host Host allocation
Constant Off Yes R All threads + host Host allocation
Texture Off Yes R All threads + host Host allocation


1.2. Host <-> Device

Host와 device의 변수가 동일한 파일에 선언되어 있어도 직접적인 참조는 불가능한다. 배열의 경우 cudaAlloc으로 할당된 포인터와 cudaMemcpy를 이용하여 Host와 device간 데이터를 교환할 수 있다. 하지만 Global scope에 __device__로 선언된 변수의 경우는 변수의 주소값과 cudaMemcpy를 이용하여 값을 전송 할 수 없다. 이런 경우에는 cudaGetSymboAddress를 이용하여 포인터를 얻어와서 cudaMemcpy를 이용하거나 cudaMemcpyToSymbol 명령을 이용하여 한다.

#include <cuda_runtime.h>
#include <stdio.h>
__device__ float devData;
__global__ void checkGlobalVariable() {
   devData +=2.0f;
}
int main(void) {
   float value = 3.14f;
   cudaMemcpyToSymbol(devData, &value, sizeof(float));
   // 위의 처럼 devData에 저장하거나
   float *dptr = NULL;
   cudaGetSymbolAddress((void**)&dptr, devData);
   cudaMemcpy(dptr, &value, sizeof(float), cudaMemcpyHostToDevice);
   //
   checkGlobalVariable <<<1, 1>>>();
   cudaMemcpyFromSymbol(&value, devData, sizeof(float));
   cudaDeviceReset();
   return EXIT_SUCCESS;
}


1.3. Pinned Memory

CPU에서 선언한 배열의 경우 Pageable 메모리 형태로 생성되므로 Device로 데이터를 전송 할때 Overhead가 생성된다. Host에서 사용할 메모리를 cudaMallocHost/cudaFreeHost를 이용하면 Pinned Memory(Non-pageable)로 선언되므로 Overhead를 줄일 수 있다. Pinned Memory가 많을 수록 host의 전체 성능이 저하 될 수 있으므로 시스템과 프로그램 상황에 맞게 조절되어야 한다.

Pinned Memory
Pinned Memory


1.4. Zero-Copy Memory

일반적으로는 Host<->Device간의 데이터를 직접 주고 받을 수 없지만 Zero-Copy memory로 선언된 영역에 대해서는 Host와 device가 접근할 수 있다. cudaHostAlloc/cudaFreeHost를 이용하여 선언/해제 할 수 있다.

Zero-Copy Memory의 장점은 다음과 같다.

  • Leveraging host memory when there is insufficient device memory
  • Avoiding explicit data transfer between the host and device
  • Improving PCIe transfer rates

큰 배열의 경우 cudaMalloc을 사용하는 것이 훨씬 효율적이다.


2. 코드

2.1. 시간 측정 함수

class Timer:
    """클래스 생성 시점부터 소멸 시점까지의 시간을 출력한다."""

    def __init__(self, func_name: str='this func'):
        self.func_name: str = func_name
        self.time_start: float = 0.0

    def __enter__(self):
        import sys
        import time
        print(f'{self.func_name} ==>', end=' ')
        sys.stdout.flush()
        self.time_start = time.perf_counter_ns()
        return self

    def __exit__(self, *args):
        import time
        time_end = time.perf_counter_ns()
        interval = (time_end - self.time_start) / 1e9
        print(f'Elapsed time: {interval:.8f} sec')


2.2. Device Info [2]

import pycuda.driver as drv
drv.init()

print('Detected {} CUDA Capable device(s) \n'.format(drv.Device.count()))

for i in range(drv.Device.count()):

    dev = drv.Device(i)
    print(f'Device {i}: {dev.name()}')
    compute_capability = float('%d.%d' % dev.compute_capability())
    print(f'  Compute Capability: {compute_capability}')
    print(f'  Total Memory: {dev.total_memory() // (1024**2)} MB')

    dev_attr_tuples = dev.get_attributes().items()
    dev_attributes = {}

    for k, v in dev_attr_tuples:
        dev_attributes[str(k)] = v

    num_mp = dev_attributes['MULTIPROCESSOR_COUNT']

    cuda_cores_per_mp = {5.0 : 128,
                         5.1 : 128,
                         5.2 : 128,
                         6.0 : 64,
                         6.1 : 128,
                         6.2 : 128}[compute_capability]
    print(f'  Multiprocessors: {num_mp}')
    print(f'  CUDA Cores Per Multiprocessor: {cuda_cores_per_mp}')
    print(f'  Total CUDA Cores: {num_mp * cuda_cores_per_mp}')

    dev_attributes.pop('MULTIPROCESSOR_COUNT')

    for k in dev_attributes.keys():
        print(f'  {k}: {dev_attributes[k]}')

Detected 1 CUDA Capable device(s) 

Device 0: GeForce GTX 850M
  Compute Capability: 5.0
  Total Memory: 2004 MB
  Multiprocessors: 5
  CUDA Cores Per Multiprocessor: 128
  Total CUDA Cores: 640
  ASYNC_ENGINE_COUNT: 1
  CAN_MAP_HOST_MEMORY: 1
  CLOCK_RATE: 901500
  COMPUTE_CAPABILITY_MAJOR: 5
  COMPUTE_CAPABILITY_MINOR: 0
  COMPUTE_MODE: DEFAULT
  CONCURRENT_KERNELS: 1
  ECC_ENABLED: 0
  GLOBAL_L1_CACHE_SUPPORTED: 0
  GLOBAL_MEMORY_BUS_WIDTH: 128
  GPU_OVERLAP: 1
  INTEGRATED: 0
  KERNEL_EXEC_TIMEOUT: 1
  L2_CACHE_SIZE: 2097152
  LOCAL_L1_CACHE_SUPPORTED: 1
  MANAGED_MEMORY: 1
  MAXIMUM_SURFACE1D_LAYERED_LAYERS: 2048
  MAXIMUM_SURFACE1D_LAYERED_WIDTH: 16384
  MAXIMUM_SURFACE1D_WIDTH: 16384
  MAXIMUM_SURFACE2D_HEIGHT: 65536
  MAXIMUM_SURFACE2D_LAYERED_HEIGHT: 16384
  MAXIMUM_SURFACE2D_LAYERED_LAYERS: 2048
  MAXIMUM_SURFACE2D_LAYERED_WIDTH: 16384
  MAXIMUM_SURFACE2D_WIDTH: 65536
  MAXIMUM_SURFACE3D_DEPTH: 4096
  MAXIMUM_SURFACE3D_HEIGHT: 4096
  MAXIMUM_SURFACE3D_WIDTH: 4096
  MAXIMUM_SURFACECUBEMAP_LAYERED_LAYERS: 2046
  MAXIMUM_SURFACECUBEMAP_LAYERED_WIDTH: 16384
  MAXIMUM_SURFACECUBEMAP_WIDTH: 16384
  MAXIMUM_TEXTURE1D_LAYERED_LAYERS: 2048
  MAXIMUM_TEXTURE1D_LAYERED_WIDTH: 16384
  MAXIMUM_TEXTURE1D_LINEAR_WIDTH: 134217728
  MAXIMUM_TEXTURE1D_MIPMAPPED_WIDTH: 16384
  MAXIMUM_TEXTURE1D_WIDTH: 65536
  MAXIMUM_TEXTURE2D_ARRAY_HEIGHT: 16384
  MAXIMUM_TEXTURE2D_ARRAY_NUMSLICES: 2048
  MAXIMUM_TEXTURE2D_ARRAY_WIDTH: 16384
  MAXIMUM_TEXTURE2D_GATHER_HEIGHT: 16384
  MAXIMUM_TEXTURE2D_GATHER_WIDTH: 16384
  MAXIMUM_TEXTURE2D_HEIGHT: 65536
  MAXIMUM_TEXTURE2D_LINEAR_HEIGHT: 65536
  MAXIMUM_TEXTURE2D_LINEAR_PITCH: 1048544
  MAXIMUM_TEXTURE2D_LINEAR_WIDTH: 65536
  MAXIMUM_TEXTURE2D_MIPMAPPED_HEIGHT: 16384
  MAXIMUM_TEXTURE2D_MIPMAPPED_WIDTH: 16384
  MAXIMUM_TEXTURE2D_WIDTH: 65536
  MAXIMUM_TEXTURE3D_DEPTH: 4096
  MAXIMUM_TEXTURE3D_DEPTH_ALTERNATE: 16384
  MAXIMUM_TEXTURE3D_HEIGHT: 4096
  MAXIMUM_TEXTURE3D_HEIGHT_ALTERNATE: 2048
  MAXIMUM_TEXTURE3D_WIDTH: 4096
  MAXIMUM_TEXTURE3D_WIDTH_ALTERNATE: 2048
  MAXIMUM_TEXTURECUBEMAP_LAYERED_LAYERS: 2046
  MAXIMUM_TEXTURECUBEMAP_LAYERED_WIDTH: 16384
  MAXIMUM_TEXTURECUBEMAP_WIDTH: 16384
  MAX_BLOCK_DIM_X: 1024
  MAX_BLOCK_DIM_Y: 1024
  MAX_BLOCK_DIM_Z: 64
  MAX_GRID_DIM_X: 2147483647
  MAX_GRID_DIM_Y: 65535
  MAX_GRID_DIM_Z: 65535
  MAX_PITCH: 2147483647
  MAX_REGISTERS_PER_BLOCK: 65536
  MAX_REGISTERS_PER_MULTIPROCESSOR: 65536
  MAX_SHARED_MEMORY_PER_BLOCK: 49152
  MAX_SHARED_MEMORY_PER_MULTIPROCESSOR: 65536
  MAX_THREADS_PER_BLOCK: 1024
  MAX_THREADS_PER_MULTIPROCESSOR: 2048
  MEMORY_CLOCK_RATE: 1001000
  MULTI_GPU_BOARD: 0
  MULTI_GPU_BOARD_GROUP_ID: 0
  PCI_BUS_ID: 1
  PCI_DEVICE_ID: 0
  PCI_DOMAIN_ID: 0
  STREAM_PRIORITIES_SUPPORTED: 1
  SURFACE_ALIGNMENT: 512
  TCC_DRIVER: 0
  TEXTURE_ALIGNMENT: 512
  TEXTURE_PITCH_ALIGNMENT: 32
  TOTAL_CONSTANT_MEMORY: 65536
  UNIFIED_ADDRESSING: 1
  WARP_SIZE: 32


2.3. Sum

GPU는 Code 최적화에 따라서 연산 시간에 많은 차이를 보인다. 간단한 SUM Code 조차도 CUDA에서 최적화 하는 것이 쉽지 않은 일이다(GPU_SUM.pdf). 하지만 Numba의 reduce를 이용하면 SUM을 CUDA로 쉽게 Coding 할 수 있다.


2.3.1. Sum Kernel Code using reduce of numba

from numba import cuda
from numba.cuda import to_device
import numpy as np

@cuda.reduce
def sum_reduce(a, b):
    return a + b


2.3.2. Sum Host Code using reduce of numba

import math
import numpy as np
from numpy import float32
from mkl_random import standard_normal

n_sample = 2 ** 26

data = float32(standard_normal(n_sample))
d_data = to_device(data.copy())

with Timer('Sum By CPU'):
    sum_cpu = np.sum(data)

_ = sum_reduce(data)
    
with Timer('Sum By GPU'):
    sum_gpu = sum_reduce(d_data)
    
print(f'CPU Result: {sum_cpu}')
print(f'GPU Result: {sum_gpu}')
Sum By CPU ==> Elapsed time: 0.02662821 sec
Sum By GPU ==> Elapsed time: 0.01087535 sec
CPU Result: 4220.08349609375
GPU Result: 4220.095703125


2.3.3. Sum Code using reduction

GPU_SUM.pdf의 예제를 Numba에서도 구현 해 볼 수 있다.

# Kernel Code
from numba import cuda
from numba.cuda import to_device
import numpy as np

@cuda.jit("void(float32[:], float32[:], int64)")
def sum_kernel(out, data, n):
    tid = cuda.threadIdx.x
    idx = cuda.grid(1)
    idx_block_start = cuda.blockIdx.x * cuda.blockDim.x

    if idx >= n:
        return

    stride = cuda.blockDim.x // 2

    while stride > 0:
        if tid < stride and (idx_block_start + tid + stride) < n:
            data[idx_block_start + tid] += data[idx_block_start + tid + stride]
        cuda.syncthreads()
        stride //= 2

    if (tid == 0):
        out[cuda.blockIdx.x] = data[idx_block_start]

# Host Code
import math
import numpy as np
from numpy import float32
from mkl_random import standard_normal

n_sample = 2 ** 26

data = float32(standard_normal(n_sample))
d_data = to_device(data.copy())

with Timer('Sum By CPU'):
    sum_cpu = np.sum(data)

threadsperblock = 128
blockspergrid = math.ceil(n_sample / threadsperblock)
out_of_gpu = cuda.device_array(blockspergrid, float32)

sum_kernel[blockspergrid, threadsperblock](out_of_gpu, data, n_sample) # For Compile

with Timer('Sum By GPU'):
    sum_kernel[blockspergrid, threadsperblock](out_of_gpu, d_data, n_sample)
    out_from_gpu = out_of_gpu.copy_to_host()
    sum_gpu = np.sum(out_from_gpu)

print(f'CPU Result: {sum_cpu}')
print(f'GPU Result: {sum_gpu}')
Sum By CPU ==> Elapsed time: 0.02695911 sec
Sum By GPU ==> Elapsed time: 0.05354665 sec
CPU Result: 458.8485107421875
GPU Result: 458.8516845703125


3. 참고자료

[1] Professional CUDA C Programming
[2] Dr. Brian Tuomanen. (2018). Chapter3, Hands-On GPU Programming with Python and CUDA (39).