🚀 CUDA 프로그래밍 입문: Python 개발자를 위한 GPU 병렬 처리 가이드
Python 개발자를 위한 CUDA 프로그래밍 가이드
딥러닝과 데이터 과학이 발전하면서 GPU를 활용한 연산이 필수적인 요소가 되었습니다. 특히, 병렬 연산이 중요한 작업에서는 CPU보다 GPU가 훨씬 강력한 성능을 발휘합니다. 하지만 대부분의 개발자는 PyTorch, TensorFlow 같은 프레임워크를 사용할 뿐, GPU 내부에서 어떤 일이 벌어지는지 깊이 이해하지 못하는 경우가 많습니다.
이 글에서는 Python 개발자를 위한 CUDA 프로그래밍 개념을 설명하고, GPU에서 연산이 어떻게 이루어지는지, CUDA를 통해 직접 병렬 처리를 최적화하는 방법을 소개하겠습니다.
1. GPU와 CPU의 차이점: 왜 CUDA가 필요한가?
CPU vs GPU
CPU는 순차 연산에 최적화된 반면, GPU는 병렬 연산에 특화되어 있습니다. CPU의 핵심은 소수의 강력한 코어, GPU의 핵심은 **수천 개의 연산 유닛(CUDA 코어)**입니다.
📌 비유하자면:
- CPU는 한 명의 천재 수학자가 한 문제를 집중해서 푸는 것과 같습니다.
- GPU는 수천 명의 초등학생이 단순한 계산을 동시에 수행하는 것과 같습니다.
이러한 차이 때문에 행렬 연산처럼 반복적인 계산이 많은 작업에서는 GPU가 CPU보다 훨씬 뛰어난 성능을 발휘합니다.
CUDA란?
NVIDIA에서 개발한 **CUDA (Compute Unified Device Architecture)**는 GPU에서 직접 실행되는 코드를 작성할 수 있도록 도와주는 플랫폼입니다.
PyTorch, TensorFlow 같은 프레임워크도 내부적으로 CUDA를 사용해 연산을 처리합니다.
하지만 CUDA를 직접 사용하면 더 세밀한 최적화가 가능하고, 최신 GPU의 성능을 최대한 활용할 수 있습니다.
2. CUDA 프로그래밍 기초: 커널과 병렬 처리
CUDA 커널 (Kernel)이란?
CUDA에서 실행되는 함수는 **커널 (Kernel)**이라고 부릅니다.
커널 함수는 한 번 실행될 때 수천 개의 스레드 (Thread)를 동시에 실행할 수 있습니다.
예제: 두 개의 벡터 더하기
아래 코드는 CUDA에서 벡터 덧셈을 수행하는 간단한 커널입니다.
__global__ void vecAddKernel(float *A, float *B, float *C, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
C[idx] = A[idx] + B[idx];
}
}
✔ __global__ 키워드는 이 함수가 GPU에서 실행될 커널임을 의미합니다.
✔ threadIdx.x와 blockIdx.x를 조합해 각 스레드가 담당할 인덱스를 계산합니다.
✔ 각 스레드는 A[idx] + B[idx] 연산을 수행합니다.
3. CUDA의 스레드와 블록 구조
CUDA에서는 수천 개의 스레드를 효과적으로 관리하기 위해 스레드 → 블록 → 그리드(Grid) 구조를 사용합니다.
- 스레드(Thread): 개별 연산을 수행하는 단위
- 블록(Block): 여러 개의 스레드가 모여 하나의 블록을 형성
- 그리드(Grid): 여러 개의 블록이 모여 전체 GPU 연산을 수행
📌 비유하자면:
- 개별 스레드는 직원
- 블록은 팀
- 그리드는 회사의 여러 부서
실제 실행 예제 (호스트 코드)
int N = 1000000;
int threadsPerBlock = 256;
int numberOfBlocks = (N + threadsPerBlock - 1) / threadsPerBlock;
// 커널 실행
vecAddKernel<<<numberOfBlocks, threadsPerBlock>>>(d_A, d_B, d_C, N);
✔ <<<numberOfBlocks, threadsPerBlock>>> 형태로 블록과 스레드 개수를 지정해 실행합니다.
✔ CUDA는 자동으로 스케줄링하여 GPU의 자원을 최적화합니다.
4. CUDA 메모리 관리와 최적화 기법
CUDA 메모리 구조
CUDA에는 다음과 같은 다양한 메모리 유형이 있습니다.
메모리 유형 특징
전역 메모리 (Global Memory) | GPU 전체에서 접근 가능하지만 속도가 느림 |
공유 메모리 (Shared Memory) | 블록 내의 스레드들이 공유하는 고속 메모리 |
레지스터 (Registers) | 가장 빠른 메모리, 각 스레드가 독립적으로 사용 |
상수 메모리 (Constant Memory) | 변하지 않는 데이터를 저장할 때 유용 |
📌 최적화 팁:
- 자주 사용하는 데이터는 공유 메모리 (Shared Memory)에 저장하면 속도가 향상됩니다.
- 스레드 간 동기화 (Synchronization) 를 통해 연산 충돌을 방지합니다.
예제: 공유 메모리 활용
__global__ void incrementElements(float *data, int n) {
__shared__ float tile[256];
int idx = blockIdx.x * blockDim.x + threadIdx.x;
int tid = threadIdx.x;
if (idx < n) {
tile[tid] = data[idx];
__syncthreads();
tile[tid] += 1.0f;
__syncthreads();
data[idx] = tile[tid];
}
}
✔ __shared__ 키워드를 사용하여 공유 메모리를 선언
✔ __syncthreads()를 사용하여 스레드 간 동기화
5. 최신 CUDA 최적화 기법: FlashAttention
딥러닝 모델(예: GPT, Transformer)의 성능을 최적화하기 위해 FlashAttention과 같은 CUDA 최적화 기술이 등장했습니다.
FlashAttention이란?
✅ GPU의 공유 메모리를 활용하여 불필요한 메모리 복사 비용을 줄이는 기법
✅ 기존 Transformer의 softmax(QK^T)V 연산을 최적화
✅ 딥러닝 훈련 속도를 3~4배 향상
import torch
def flash_attention_pytorch(Q, K, V):
scale = 1 / (Q.shape[-1] ** 0.5)
S = (Q @ K.T) * scale
A = torch.softmax(S, dim=-1)
return A @ V
✔ PyTorch에서 @ 연산자를 사용하면 내부적으로 CUDA 커널이 실행됩니다.
✔ FlashAttention과 같은 기법을 직접 구현하면 더 빠른 연산이 가능합니다.
CUDA 프로그래밍을 배우면 GPU 성능을 극대화할 수 있다!
이 글에서는 CUDA 프로그래밍의 핵심 개념을 살펴보았습니다.
✅ CUDA를 사용하면 GPU를 직접 제어하여 최적의 성능을 끌어낼 수 있습니다.
✅ PyTorch 같은 프레임워크를 활용하는 것뿐만 아니라, 직접 CUDA 커널을 작성하면 더 세밀한 최적화가 가능합니다.
✅ FlashAttention 같은 최신 기법을 활용하면 딥러닝 성능을 극대화할 수 있습니다.
https://www.pyspur.dev/blog/introduction_cuda_programming