Skip to content
Published on

CUDA Hands-on 완벽 가이드: GPU 컴퓨팅의 모든 것

Authors
  • Name
    Twitter

1. 서론: CUDA란 무엇인가

1.1 CUDA의 정의

**CUDA(Compute Unified Device Architecture)**는 NVIDIA가 개발한 병렬 컴퓨팅 플랫폼이자 프로그래밍 모델이다. GPU의 수천 개 코어를 활용하여 범용 연산(GPGPU)을 수행할 수 있게 해주며, 2007년 첫 출시 이후 AI, 과학 시뮬레이션, 영상 처리, 금융 모델링 등 다양한 분야에서 사실상의 표준으로 자리잡았다.

2025년 기준 최신 CUDA Toolkit은 13.1 버전까지 출시되었으며, CUDA 13.1에서는 CUDA Tile이라는 타일 기반 프로그래밍 모델과 cuTile Python DSL이 도입되어 Tensor Core 추상화와 차세대 Blackwell GPU에 대한 전방 호환성을 제공한다.

1.2 GPU 컴퓨팅의 역사와 CUDA의 등장 배경

GPU 컴퓨팅의 진화를 시대별로 정리하면 다음과 같다.

시기사건의의
2001GPGPU 연구 시작셰이더를 이용한 범용 연산 시도
2006NVIDIA Tesla 아키텍처 발표최초의 통합 셰이더 아키텍처
2007CUDA 1.0 출시GPU 범용 컴퓨팅의 시작
2012AlexNet (ImageNet)GPU 딥러닝 시대 개막
2017Volta + Tensor Core혼합 정밀도 연산 가속
2020Ampere (A100)TF32, Sparsity 지원
2022Hopper (H100)Transformer Engine, FP8 지원
2024Blackwell (B200)FP4, 5세대 Tensor Core
2025CUDA 13.0/13.1 출시CUDA Tile, cuTile DSL 도입

1.3 CPU vs GPU 아키텍처 비교

CPU와 GPU는 근본적으로 다른 설계 철학을 가진다.

특성CPUGPU
설계 목표낮은 레이턴시 (직렬 처리)높은 처리량 (병렬 처리)
코어 수수 개 ~ 수십 개수천 ~ 수만 개
클럭 속도높음 (4~6 GHz)상대적으로 낮음 (1~2 GHz)
캐시 크기큼 (수십 MB)작음 (코어당)
제어 로직복잡 (분기 예측, OoO 실행)단순 (다수 스레드로 레이턴시 은닉)
병렬화 모델SIMD (Single Instruction Multiple Data)SIMT (Single Instruction Multiple Threads)
최적 워크로드복잡한 분기, 순차 로직대규모 데이터 병렬 연산

SIMD vs SIMT 핵심 차이: SIMD는 하나의 명령으로 여러 데이터를 동시에 처리하는 벡터 연산 방식이다. SIMT는 여기서 한 걸음 더 나아가, 각 스레드가 독립적인 프로그램 카운터를 가지면서도 동일한 명령을 동시에 실행한다. 분기(branch)가 발생하면 Warp 내 스레드들이 서로 다른 경로를 실행할 수 있지만, 이때 Warp Divergence가 발생하여 성능이 저하된다.

1.4 CUDA가 AI/ML/딥러닝에서 필수인 이유

현대 AI/ML 워크로드에서 CUDA가 핵심인 이유는 명확하다.

  • 행렬 연산 가속: 딥러닝의 핵심인 행렬 곱셈(GEMM)을 Tensor Core로 극적으로 가속
  • 소프트웨어 생태계: cuDNN, cuBLAS, NCCL, TensorRT 등 최적화된 라이브러리 생태계
  • 프레임워크 지원: PyTorch, TensorFlow, JAX 등 모든 주요 프레임워크가 CUDA를 기본 백엔드로 사용
  • 혼합 정밀도: FP16, BF16, FP8, FP4까지 저정밀도 연산으로 학습/추론 속도 극대화
  • Multi-GPU 스케일링: NVLink, NVSwitch를 통한 수백 GPU 분산 학습

2. GPU 아키텍처 기초

2.1 NVIDIA GPU 내부 구조

NVIDIA GPU의 핵심 구성 요소를 계층적으로 살펴보자.

GPU (GPC - Graphics Processing Cluster)
├── SM (Streaming Multiprocessor) × N│   ├── CUDA Core (FP32/INT32) × 128 (Hopper 기준)
│   ├── Tensor Core × 4 (4세대, Hopper 기준)
│   ├── RT Core (Ray Tracing) × 1│   ├── Warp Scheduler × 4│   ├── Register File (256 KB)
│   ├── L1 Cache / Shared Memory (공유, 최대 228 KB)
│   └── SFU (Special Function Unit)
├── L2 Cache (공유)
├── Memory Controller
└── HBM (High Bandwidth Memory)

주요 코어 유형 설명:

  • CUDA Core: 범용 부동소수점/정수 연산을 수행하는 기본 처리 유닛. FP32, FP64, INT32 연산 담당
  • Tensor Core: 행렬 곱셈-누적(MMA, Matrix Multiply-Accumulate) 연산에 특화된 코어. 딥러닝 학습/추론의 핵심 가속기
  • RT Core: 광선 추적(Ray Tracing) 가속 전용 하드웨어. 주로 그래픽스 워크로드에서 사용

2.2 메모리 계층 구조

GPU 메모리는 속도와 크기에 따라 계층적으로 구성된다.

메모리 유형위치크기 (H100 기준)대역폭접근 범위특성
RegisterSM 내부256 KB/SM최고속Thread 전용가장 빠르나 한정적
Shared MemorySM 내부최대 228 KB/SM매우 빠름Block 내 공유프로그래머 관리 캐시
L1 CacheSM 내부Shared와 공유매우 빠름SM 전용HW 관리
L2 CacheGPU 전역50 MB빠름전체 SM 공유HW 관리
Global Memory (HBM)GPU 외부80 GB3.35 TB/s전체 접근가장 크나 레이턴시 높음
Constant MemoryGPU 전역64 KB캐시 시 빠름읽기 전용Broadcast 최적화
Texture MemoryGPU 전역Global과 공유캐시 시 빠름읽기 전용2D 공간 지역성 최적화
속도:  Register > Shared/L1 > L2 > Global (HBM)
크기:  Global (HBM) > L2 > Shared/L1 > Register

2.3 Warp, Block, Grid 개념

CUDA의 실행 모델은 3단계 계층 구조를 가진다.

Grid (커널 실행 단위)
├── Block (0,0)          Block (1,0)          Block (2,0)
│   ├── Warp 0           ├── Warp 0           ├── Warp 0
│   │   ├── Thread 0     │   ├── Thread 0     │   ├── Thread 0
│   │   ├── Thread 1     │   ├── Thread 1     │   ├── Thread 1
│   │   ├── ...          │   ├── ...          │   ├── ...
│   │   └── Thread 31    │   └── Thread 31    │   └── Thread 31
│   ├── Warp 1           ├── Warp 1           ├── Warp 1
│   └── ...              └── ...              └── ...
├── Block (0,1)          Block (1,1)          ...
└── ...
  • Thread: GPU 연산의 최소 실행 단위
  • Warp: 32개 Thread로 구성된 실행 그룹. SM에서 동시에 같은 명령을 실행하는 단위 (SIMT의 핵심)
  • Block (Thread Block): 여러 Warp로 구성. 동일 SM에서 실행되며, Shared Memory를 공유
  • Grid: 전체 커널 실행을 구성하는 Block들의 집합

핵심 제약 사항:

항목제한값 (Compute Capability 9.0 기준)
Block당 최대 Thread 수1,024
Warp 크기32 (고정)
Block 차원 최대값(1024, 1024, 64)
Grid 차원 최대값(2^31-1, 65535, 65535)
SM당 최대 Block 수32
SM당 최대 Warp 수64

2.4 Compute Capability 버전별 차이

Compute Capability(CC)는 GPU 하드웨어의 기능 집합을 정의한다.

CC아키텍처대표 GPU주요 특징
7.0VoltaV1001세대 Tensor Core, 독립 스레드 스케줄링
7.5TuringRTX 2080INT8/INT4 Tensor Core, RT Core
8.0AmpereA1003세대 Tensor Core, TF32, Sparsity
8.6AmpereRTX 3090소비자용 Ampere
8.9Ada LovelaceRTX 40904세대 Tensor Core, FP8, DLSS 3
9.0HopperH1004세대 Tensor Core, Transformer Engine, DPX
10.0BlackwellB2005세대 Tensor Core, FP4, TMEM
12.0Blackwell UltraB3005세대 Tensor Core 강화, 288 GB HBM3E

참고로 CUDA 13.0부터는 Maxwell(CC 5.x), Pascal(CC 6.x), Volta(CC 7.0)에 대한 지원이 제거되었다.

2.5 최신 GPU 세대 비교

데이터센터용 GPU 3세대를 비교한다.

사양A100 (SXM)H100 (SXM5)B200 (SXM)B300 (SXM)
아키텍처AmpereHopperBlackwellBlackwell Ultra
CUDA Core6,91216,89618,43218,432+
Tensor Core432 (3세대)528 (4세대)5세대5세대
메모리80 GB HBM2e80 GB HBM3180 GB HBM3E288 GB HBM3E
메모리 대역폭2.0 TB/s3.35 TB/s7.7 TB/s8.0 TB/s
FP32 성능19.5 TFLOPS60 TFLOPS비공개비공개
FP16 Tensor312 TFLOPS990 TFLOPS비공개비공개
FP4 Tensor미지원미지원9.0 PFLOPS14.0 PFLOPS
NVLink3세대 (600 GB/s)4세대 (900 GB/s)5세대 (1.8 TB/s)5세대 (1.8 TB/s)
TDP400W700W1,000W1,400W
냉각공냉/수냉공냉/수냉수냉 권장수냉 필수 (DLC)

B200은 A100 대비 학습 성능 3배, 추론 성능 15배 향상을 달성했다. B300(Blackwell Ultra)은 FP4 연산에서 14 PFLOPS로 B200 대비 55.6% 빠른 성능을 제공한다.


3. CUDA 개발 환경 설정

3.1 CUDA Toolkit 설치

Linux (Ubuntu/Debian)

# 1. NVIDIA 드라이버 확인
nvidia-smi

# 2. CUDA Keyring 추가 (Ubuntu 22.04 예시)
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update

# 3. CUDA Toolkit 설치
sudo apt-get install cuda-toolkit-13-1

# 4. 환경 변수 설정
export PATH=/usr/local/cuda-13.1/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-13.1/lib64:$LD_LIBRARY_PATH

# 5. 설치 확인
nvcc --version

Windows

# 1. NVIDIA 공식 사이트에서 CUDA Toolkit 다운로드
# https://developer.nvidia.com/cuda-downloads

# 2. 설치 후 환경 변수 확인
echo %CUDA_PATH%
# 일반적으로 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1

# 3. 확인
nvcc --version
nvidia-smi

3.2 nvidia-smi 명령어 활용

nvidia-smi는 GPU 상태를 모니터링하는 핵심 도구이다.

# 기본 정보 출력
nvidia-smi

# 출력 예시:
# +-----------------------------------------------------------------------------------------+
# | NVIDIA-SMI 560.35.03    Driver Version: 560.35.03    CUDA Version: 13.1                 |
# |-----------------------------------------+------------------------+----------------------|
# | GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
# | Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
# |=========================================+========================+======================|
# |   0  NVIDIA H100 80GB HBM3          On | 00000000:3B:00.0   Off |                    0 |
# | N/A   32C    P0              72W / 700W |    1234MiB / 81559MiB  |      0%      Default |
# +-----------------------------------------+------------------------+----------------------+

# 지속적 모니터링 (1초 간격)
nvidia-smi -l 1

# dmon - GPU 메트릭 모니터링
nvidia-smi dmon -s pucvmet -d 1

# 특정 GPU 정보
nvidia-smi -i 0 -q

# GPU 프로세스 확인
nvidia-smi pmon -i 0

# GPU 클럭, 메모리 정보
nvidia-smi -q -d CLOCK,MEMORY

# CSV 형식으로 출력 (스크립트용)
nvidia-smi --query-gpu=name,temperature.gpu,utilization.gpu,memory.used,memory.total \
  --format=csv,noheader,nounits

3.3 nvcc 컴파일러 사용법

nvcc는 CUDA 소스 코드를 컴파일하는 NVIDIA CUDA Compiler이다.

# 기본 컴파일
nvcc -o hello hello.cu

# 특정 아키텍처 타겟
nvcc -arch=sm_90 -o kernel kernel.cu   # Hopper H100
nvcc -arch=sm_80 -o kernel kernel.cu   # Ampere A100

# 디버그 모드
nvcc -g -G -o debug_kernel kernel.cu

# 최적화 레벨
nvcc -O3 -o optimized kernel.cu

# PTX 코드 생성 (중간 표현)
nvcc -ptx kernel.cu

# 여러 아키텍처 동시 지원 (Fat Binary)
nvcc -gencode arch=compute_80,code=sm_80 \
     -gencode arch=compute_90,code=sm_90 \
     -o multi_arch kernel.cu

# 라이브러리 링크
nvcc -lcublas -lcurand -o linked_kernel kernel.cu

# 상세 컴파일 정보
nvcc --resource-usage -o kernel kernel.cu

3.4 CUDA 버전 확인 및 호환성

CUDA에는 두 가지 버전이 존재하며, 이를 혼동하지 않는 것이 중요하다.

# 1. 드라이버 지원 CUDA 버전 (Driver API)
nvidia-smi
# 우측 상단에 "CUDA Version: 13.1" 표시
# 이 버전은 드라이버가 지원하는 최대 CUDA 런타임 버전

# 2. CUDA Toolkit 버전 (Runtime API)
nvcc --version
# "release 13.1" 표시
# 실제 설치된 CUDA Toolkit 버전

# 3. 런타임에서 확인
python3 -c "import torch; print(torch.version.cuda)"

핵심 호환성 규칙:

  • 드라이버 CUDA 버전이 Toolkit CUDA 버전 이상이어야 함
  • 예: 드라이버가 CUDA 13.1을 지원하면 CUDA 12.x Toolkit도 사용 가능 (하위 호환)
  • CUDA Toolkit의 Minor Version Compatibility 정책에 따라 같은 Major 버전 내에서는 바이너리 호환

3.5 Docker에서 CUDA 사용

NVIDIA Container Toolkit을 사용하면 Docker 컨테이너에서 GPU를 활용할 수 있다.

# 1. NVIDIA Container Toolkit 설치 (Ubuntu)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# 2. Docker 런타임 설정
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

# 3. GPU 컨테이너 실행
docker run --rm --gpus all nvidia/cuda:13.1.0-base-ubuntu22.04 nvidia-smi

# 특정 GPU만 사용
docker run --rm --gpus '"device=0,1"' nvidia/cuda:13.1.0-base-ubuntu22.04 nvidia-smi

# docker compose에서 GPU 사용
# docker-compose.yml:
# docker-compose.yml
services:
  gpu-app:
    image: nvidia/cuda:13.1.0-devel-ubuntu22.04
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    command: nvidia-smi

3.6 conda/pip으로 PyTorch/TensorFlow CUDA 설치

# PyTorch (CUDA 12.4 빌드 예시)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124

# conda를 사용하는 경우
conda install pytorch torchvision torchaudio pytorch-cuda=12.4 -c pytorch -c nvidia

# TensorFlow (GPU 자동 감지)
pip install tensorflow[and-cuda]

# CUDA 사용 가능 여부 확인 (Python)
python3 -c "
import torch
print(f'PyTorch version: {torch.__version__}')
print(f'CUDA available: {torch.cuda.is_available()}')
print(f'CUDA version: {torch.version.cuda}')
print(f'GPU count: {torch.cuda.device_count()}')
if torch.cuda.is_available():
    print(f'GPU name: {torch.cuda.get_device_name(0)}')
"

4. CUDA 프로그래밍 기초 (C/C++)

4.1 Host(CPU) vs Device(GPU) 코드

CUDA 프로그래밍에서는 CPU 측 코드를 Host, GPU 측 코드를 Device라 부른다.

┌─────────────────────────────────────────────────────────┐
Host (CPU)│  ┌─────────────────────────────────────────────────────┐│
│  │ 1. 데이터 준비 (Host Memory)                       ││
│  │ 2. GPU 메모리 할당 (cudaMalloc)                    ││
│  │ 3. 데이터 전송: Host -> Device (cudaMemcpy)        ││
│  │ 4. 커널 실행 (<<<grid, block>>>)                   ││
│  │ 5. 결과 전송: Device -> Host (cudaMemcpy)          ││
│  │ 6. GPU 메모리 해제 (cudaFree)                      ││
│  └─────────────────────────────────────────────────────┘│
│                        ↕ PCIe / NVLink│  ┌─────────────────────────────────────────────────────┐│
│  │  Device (GPU)                                       ││
│  │  - 커널 함수 병렬 실행                              ││
│  │  - 수천 스레드가 동시 처리                           ││
│  └─────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────┘

4.2 함수 한정자 (Function Qualifiers)

CUDA는 세 가지 함수 한정자를 제공한다.

한정자실행 위치호출 가능 위치설명
__global__Device (GPU)Host (CPU)커널 함수. 반환형은 반드시 void
__device__Device (GPU)Device (GPU)GPU 내부에서만 호출 가능한 헬퍼 함수
__host__Host (CPU)Host (CPU)일반 CPU 함수 (기본값, 생략 가능)
__host__ __device__둘 다둘 다CPU/GPU 모두에서 사용 가능
// __global__: 커널 함수 - Host에서 호출, Device에서 실행
__global__ void myKernel(float* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        data[idx] *= 2.0f;
    }
}

// __device__: Device 전용 헬퍼 함수
__device__ float square(float x) {
    return x * x;
}

// __host__ __device__: CPU/GPU 겸용 함수
__host__ __device__ float add(float a, float b) {
    return a + b;
}

4.3 커널 호출 문법

커널 함수는 특수한 삼중 꺾쇠 문법으로 호출한다.

// 커널 호출 문법
// kernel<<<gridDim, blockDim, sharedMemSize, stream>>>(args...);
//
// gridDim:       Grid 내 Block 수 (dim3)
// blockDim:      Block 내 Thread 수 (dim3)
// sharedMemSize: 동적 Shared Memory 크기 (바이트, 선택)
// stream:        실행 스트림 (선택)

// 1D 예시: 256개 스레드, 1개 블록
myKernel<<<1, 256>>>(d_data, n);

// 1D 예시: N개 데이터를 256 스레드 블록으로 처리
int threadsPerBlock = 256;
int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
myKernel<<<blocksPerGrid, threadsPerBlock>>>(d_data, n);

// 2D 예시: 이미지 처리
dim3 blockDim(16, 16);       // 16x16 = 256 스레드/블록
dim3 gridDim(
    (width + 15) / 16,
    (height + 15) / 16
);
imageKernel<<<gridDim, blockDim>>>(d_image, width, height);

4.4 Thread 계층 구조

각 스레드는 내장 변수를 통해 자신의 위치를 알 수 있다.

// 내장 변수 (Built-in Variables)
threadIdx.x, threadIdx.y, threadIdx.z  // Block 내 Thread 인덱스
blockIdx.x,  blockIdx.y,  blockIdx.z   // Grid 내 Block 인덱스
blockDim.x,  blockDim.y,  blockDim.z   // Block의 차원 크기
gridDim.x,   gridDim.y,   gridDim.z    // Grid의 차원 크기

// 글로벌 Thread ID 계산 (1D)
int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;

// 글로벌 Thread ID 계산 (2D)
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
int globalIdx = row * width + col;

// 전체 스레드 수 (Grid-stride loop용)
int totalThreads = gridDim.x * blockDim.x;

4.5 메모리 관리 함수

// GPU 메모리 할당
float* d_data;
cudaMalloc((void**)&d_data, n * sizeof(float));

// Host -> Device 복사
cudaMemcpy(d_data, h_data, n * sizeof(float), cudaMemcpyHostToDevice);

// Device -> Host 복사
cudaMemcpy(h_data, d_data, n * sizeof(float), cudaMemcpyDeviceToHost);

// GPU 메모리 해제
cudaFree(d_data);

// GPU 메모리 초기화
cudaMemset(d_data, 0, n * sizeof(float));

// 에러 체크 매크로 (필수!)
#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA Error at %s:%d - %s\n", \
                    __FILE__, __LINE__, cudaGetErrorString(err)); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

// 사용 예
CUDA_CHECK(cudaMalloc((void**)&d_data, n * sizeof(float)));
CUDA_CHECK(cudaMemcpy(d_data, h_data, n * sizeof(float), cudaMemcpyHostToDevice));

4.6 Hello World 예제

// hello_cuda.cu
#include <stdio.h>

__global__ void helloKernel() {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    printf("Hello from GPU thread %d (block %d, thread %d)\n",
           tid, blockIdx.x, threadIdx.x);
}

int main() {
    // 2개 블록, 각 4개 스레드 = 총 8개 스레드
    helloKernel<<<2, 4>>>();

    // GPU 작업 완료 대기
    cudaDeviceSynchronize();

    printf("Hello from CPU!\n");
    return 0;
}
# 컴파일 및 실행
nvcc -o hello hello_cuda.cu
./hello

4.7 벡터 덧셈 예제

CUDA의 "Hello World"라 할 수 있는 벡터 덧셈 예제이다.

// vector_add.cu
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>

#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA Error: %s at %s:%d\n", \
                    cudaGetErrorString(err), __FILE__, __LINE__); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

// GPU 커널: 벡터 덧셈
__global__ void vectorAdd(const float* A, const float* B, float* C, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        C[idx] = A[idx] + B[idx];
    }
}

int main() {
    const int N = 1 << 20;  // 1M 원소
    size_t bytes = N * sizeof(float);

    // Host 메모리 할당 및 초기화
    float *h_A = (float*)malloc(bytes);
    float *h_B = (float*)malloc(bytes);
    float *h_C = (float*)malloc(bytes);

    for (int i = 0; i < N; i++) {
        h_A[i] = (float)i;
        h_B[i] = (float)(i * 2);
    }

    // Device 메모리 할당
    float *d_A, *d_B, *d_C;
    CUDA_CHECK(cudaMalloc(&d_A, bytes));
    CUDA_CHECK(cudaMalloc(&d_B, bytes));
    CUDA_CHECK(cudaMalloc(&d_C, bytes));

    // Host -> Device 복사
    CUDA_CHECK(cudaMemcpy(d_A, h_A, bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_B, h_B, bytes, cudaMemcpyHostToDevice));

    // 커널 실행
    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
    vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);

    // 에러 체크
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    // Device -> Host 복사
    CUDA_CHECK(cudaMemcpy(h_C, d_C, bytes, cudaMemcpyDeviceToHost));

    // 결과 검증
    for (int i = 0; i < N; i++) {
        if (fabs(h_C[i] - (h_A[i] + h_B[i])) > 1e-5) {
            fprintf(stderr, "Verification failed at index %d!\n", i);
            exit(EXIT_FAILURE);
        }
    }
    printf("Vector addition of %d elements: SUCCESS\n", N);

    // 메모리 해제
    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    free(h_A); free(h_B); free(h_C);

    return 0;
}

4.8 행렬 곱셈 예제

// matmul.cu
#include <stdio.h>
#include <cuda_runtime.h>

#define TILE_SIZE 16

// 기본 행렬 곱셈 커널
__global__ void matMulBasic(const float* A, const float* B, float* C,
                            int M, int N, int K) {
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;

    if (row < M && col < N) {
        float sum = 0.0f;
        for (int k = 0; k < K; k++) {
            sum += A[row * K + k] * B[k * N + col];
        }
        C[row * N + col] = sum;
    }
}

// Shared Memory를 활용한 Tiled 행렬 곱셈 (최적화 버전)
__global__ void matMulTiled(const float* A, const float* B, float* C,
                            int M, int N, int K) {
    __shared__ float tileA[TILE_SIZE][TILE_SIZE];
    __shared__ float tileB[TILE_SIZE][TILE_SIZE];

    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    float sum = 0.0f;

    // 타일 단위로 반복
    for (int t = 0; t < (K + TILE_SIZE - 1) / TILE_SIZE; t++) {
        // Shared Memory로 타일 로드
        if (row < M && (t * TILE_SIZE + threadIdx.x) < K)
            tileA[threadIdx.y][threadIdx.x] = A[row * K + t * TILE_SIZE + threadIdx.x];
        else
            tileA[threadIdx.y][threadIdx.x] = 0.0f;

        if ((t * TILE_SIZE + threadIdx.y) < K && col < N)
            tileB[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        else
            tileB[threadIdx.y][threadIdx.x] = 0.0f;

        __syncthreads();  // 타일 로드 완료 대기

        // 타일 내 곱셈-누적
        for (int k = 0; k < TILE_SIZE; k++) {
            sum += tileA[threadIdx.y][k] * tileB[k][threadIdx.x];
        }

        __syncthreads();  // 다음 타일 로드 전 동기화
    }

    if (row < M && col < N) {
        C[row * N + col] = sum;
    }
}

int main() {
    const int M = 1024, N = 1024, K = 1024;

    // ... (메모리 할당, 초기화, 데이터 전송 생략)

    dim3 blockDim(TILE_SIZE, TILE_SIZE);
    dim3 gridDim((N + TILE_SIZE - 1) / TILE_SIZE,
                 (M + TILE_SIZE - 1) / TILE_SIZE);

    matMulTiled<<<gridDim, blockDim>>>(d_A, d_B, d_C, M, N, K);

    // ... (결과 복사, 검증, 메모리 해제 생략)
    return 0;
}

5. CUDA 메모리 관리 심층

5.1 Global Memory와 Coalesced Access

Global Memory 접근 시 **Coalesced Access(합쳤된 접근)**는 성능의 핵심이다. Warp 내 32개 스레드가 연속적인 메모리 주소에 접근하면, GPU가 이를 하나의 트랜잭션으로 묶어 처리한다.

// Coalesced Access (좋은 패턴)
// Warp 내 스레드들이 연속 메모리에 접근
__global__ void coalesced(float* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        data[idx] = data[idx] * 2.0f;  // 연속 접근
    }
}

// Non-coalesced Access (나쁜 패턴)
// 스레드가 stride를 가지고 비연속적으로 접근
__global__ void strided(float* data, int n, int stride) {
    int idx = (blockIdx.x * blockDim.x + threadIdx.x) * stride;
    if (idx < n) {
        data[idx] = data[idx] * 2.0f;  // stride만큼 건너뛴 접근
    }
}

// AoS(Array of Structures) vs SoA(Structure of Arrays)
// AoS: Non-coalesced (나쁨)
struct ParticleAoS {
    float x, y, z;
    float vx, vy, vz;
};
// 스레드 0: particle[0].x, 스레드 1: particle[1].x -> stride 접근

// SoA: Coalesced (좋음)
struct ParticlesSoA {
    float* x;  float* y;  float* z;
    float* vx; float* vy; float* vz;
};
// 스레드 0: x[0], 스레드 1: x[1] -> 연속 접근

5.2 Shared Memory와 Bank Conflict

Shared Memory는 SM 내 스레드 블록이 공유하는 고속 메모리이다. 32개의 Bank로 구성되며, 서로 다른 스레드가 같은 Bank에 동시 접근하면 Bank Conflict가 발생한다.

// Shared Memory 사용 예시
__global__ void sharedMemExample(float* input, float* output, int n) {
    // 정적 할당
    __shared__ float sharedData[256];

    int tid = threadIdx.x;
    int gid = blockIdx.x * blockDim.x + threadIdx.x;

    // Global -> Shared 로드
    if (gid < n) {
        sharedData[tid] = input[gid];
    }
    __syncthreads();  // 모든 스레드가 로드 완료할 때까지 대기

    // Shared Memory에서 연산 (이웃 원소 합산 예시)
    if (tid > 0 && tid < blockDim.x - 1 && gid < n) {
        output[gid] = sharedData[tid - 1] + sharedData[tid] + sharedData[tid + 1];
    }
}

// Bank Conflict 회피: 패딩 기법
// 문제: 32x32 행렬의 열(column) 접근 시 모든 스레드가 같은 Bank에 매핑
__shared__ float tile[32][32];       // Bank Conflict 발생!
__shared__ float tile[32][32 + 1];   // 패딩으로 Bank Conflict 회피

5.3 Unified Memory

Unified Memory는 CPU와 GPU가 동일한 포인터로 메모리에 접근할 수 있게 해주는 추상화 계층이다.

// Unified Memory 사용
float* data;
cudaMallocManaged(&data, N * sizeof(float));

// CPU에서 초기화
for (int i = 0; i < N; i++) {
    data[i] = (float)i;
}

// GPU 커널 실행 - 별도 cudaMemcpy 불필요!
myKernel<<<blocks, threads>>>(data, N);
cudaDeviceSynchronize();

// CPU에서 결과 바로 접근
printf("Result: %f\n", data[0]);

// 해제
cudaFree(data);

// 메모리 프리페치 힌트 (성능 최적화)
cudaMemPrefetchAsync(data, N * sizeof(float), deviceId);  // GPU로 프리페치
cudaMemPrefetchAsync(data, N * sizeof(float), cudaCpuDeviceId);  // CPU로 프리페치

5.4 Pinned Memory

Pinned Memory(Page-locked Memory)는 OS의 페이지 스왑 대상에서 제외된 호스트 메모리로, GPU와의 데이터 전송 속도를 높인다.

// Pinned Memory 할당
float* h_pinned;
cudaMallocHost(&h_pinned, N * sizeof(float));  // 또는 cudaHostAlloc

// 일반 메모리 대비 전송 속도 비교
// Pageable:  ~12 GB/s (PCIe Gen4)
// Pinned:    ~25 GB/s (PCIe Gen4) - 약 2배 빠름

// 비동기 전송에 필수
cudaMemcpyAsync(d_data, h_pinned, bytes, cudaMemcpyHostToDevice, stream);

// 해제
cudaFreeHost(h_pinned);

5.5 Memory Pool

CUDA 11.2부터 도입된 Memory Pool은 반복적인 메모리 할당/해제 오버헤드를 줄인다.

// Stream-ordered Memory Allocator (CUDA 11.2+)
float* d_data;
cudaMallocAsync(&d_data, bytes, stream);

// 사용 후 비동기 해제
cudaFreeAsync(d_data, stream);

// Memory Pool 설정
cudaMemPool_t mempool;
cudaDeviceGetDefaultMemPool(&mempool, deviceId);

// Pool 크기 제한 설정
uint64_t threshold = 1ULL << 30;  // 1 GB
cudaMemPoolSetAttribute(mempool, cudaMemPoolAttrReleaseThreshold, &threshold);

5.6 메모리 대역폭 최적화 요약

기법효과적용 난이도
Coalesced AccessGlobal Memory 처리량 극대화낮음
SoA 레이아웃Coalesced Access 보장중간
Shared Memory TilingGlobal Memory 접근 횟수 감소중간
Bank Conflict 회피 (패딩)Shared Memory 처리량 극대화낮음
Pinned MemoryHost-Device 전송 2배 가속낮음
Unified Memory + Prefetch프로그래밍 편의 + 성능낮음
Memory Pool할당/해제 오버헤드 제거낮음
Texture Memory2D 공간 지역성 활용중간

6. CUDA 스트림과 비동기 실행

6.1 CUDA Stream 개념

CUDA Stream은 GPU에서 순서대로 실행되는 명령 시퀀스이다. 서로 다른 Stream의 작업은 동시에 실행될 수 있다.

Default Stream (Stream 0):
[MemcpyH2D] -> [Kernel A] -> [MemcpyD2H]
                                           (순차 실행, 파이프라인 미활용)

Multi-Stream:
Stream 1: [MemcpyH2D_1] -> [Kernel_1] -> [MemcpyD2H_1]
Stream 2:     [MemcpyH2D_2] -> [Kernel_2] -> [MemcpyD2H_2]
Stream 3:         [MemcpyH2D_3] -> [Kernel_3] -> [MemcpyD2H_3]
                                           (오버랩 실행, GPU 활용 극대화)

6.2 Stream 생성 및 사용

// Stream 생성
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);

// 비동기 메모리 전송 (Pinned Memory 필수!)
cudaMemcpyAsync(d_A, h_A, bytes, cudaMemcpyHostToDevice, stream1);
cudaMemcpyAsync(d_B, h_B, bytes, cudaMemcpyHostToDevice, stream2);

// 커널 실행 (특정 Stream에서)
kernelA<<<grid, block, 0, stream1>>>(d_A);
kernelB<<<grid, block, 0, stream2>>>(d_B);

// 결과 비동기 복사
cudaMemcpyAsync(h_A, d_A, bytes, cudaMemcpyDeviceToHost, stream1);
cudaMemcpyAsync(h_B, d_B, bytes, cudaMemcpyDeviceToHost, stream2);

// Stream 동기화
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);

// Stream 해제
cudaStreamDestroy(stream1);
cudaStreamDestroy(stream2);

6.3 Multi-Stream 파이프라인 패턴

const int NUM_STREAMS = 4;
const int CHUNK_SIZE = N / NUM_STREAMS;

cudaStream_t streams[NUM_STREAMS];
for (int i = 0; i < NUM_STREAMS; i++) {
    cudaStreamCreate(&streams[i]);
}

// Pinned Memory 할당
float *h_in, *h_out;
cudaMallocHost(&h_in, N * sizeof(float));
cudaMallocHost(&h_out, N * sizeof(float));

float *d_in, *d_out;
cudaMalloc(&d_in, N * sizeof(float));
cudaMalloc(&d_out, N * sizeof(float));

// 파이프라인 실행
for (int i = 0; i < NUM_STREAMS; i++) {
    int offset = i * CHUNK_SIZE;
    size_t chunkBytes = CHUNK_SIZE * sizeof(float);

    // 1. Host -> Device (비동기)
    cudaMemcpyAsync(d_in + offset, h_in + offset,
                    chunkBytes, cudaMemcpyHostToDevice, streams[i]);

    // 2. 커널 실행
    int blocks = (CHUNK_SIZE + 255) / 256;
    processKernel<<<blocks, 256, 0, streams[i]>>>(
        d_in + offset, d_out + offset, CHUNK_SIZE);

    // 3. Device -> Host (비동기)
    cudaMemcpyAsync(h_out + offset, d_out + offset,
                    chunkBytes, cudaMemcpyDeviceToHost, streams[i]);
}

// 전체 완료 대기
cudaDeviceSynchronize();

6.4 CUDA Events (타이밍 측정)

cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);

// 타이밍 시작
cudaEventRecord(start);

// 커널 실행
myKernel<<<grid, block>>>(d_data, N);

// 타이밍 종료
cudaEventRecord(stop);
cudaEventSynchronize(stop);

// 경과 시간 계산
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
printf("Kernel execution time: %.3f ms\n", milliseconds);

// 대역폭 계산
float bandwidth = (bytes_read + bytes_written) / (milliseconds * 1e6);
printf("Effective bandwidth: %.2f GB/s\n", bandwidth);

cudaEventDestroy(start);
cudaEventDestroy(stop);

6.5 동기화 함수 비교

함수범위용도
cudaDeviceSynchronize()전체 Device모든 Stream 완료 대기
cudaStreamSynchronize(stream)특정 Stream해당 Stream 완료 대기
cudaEventSynchronize(event)특정 Event해당 Event 완료 대기
cudaStreamWaitEvent(stream, event)Stream 간Stream이 Event 완료 대기 후 진행
__syncthreads()Block 내부Block 내 스레드 동기화 (커널 내)

7. CUDA 최적화 기법

7.1 Occupancy 최적화

Occupancy는 SM에서 실제 활성 Warp 수 대비 최대 지원 Warp 수의 비율이다. 높은 Occupancy가 항상 최고 성능을 의미하지는 않지만, 메모리 레이턴시를 은닉하는 데 핵심적이다.

// Occupancy 계산 API
int blockSize;
int minGridSize;

// 최적 블록 크기 자동 계산
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, myKernel, 0, 0);
printf("Optimal block size: %d\n", blockSize);
printf("Minimum grid size: %d\n", minGridSize);

// 특정 블록 크기에서의 Occupancy 확인
int maxActiveBlocks;
cudaOccupancyMaxActiveBlocksPerMultiprocessor(
    &maxActiveBlocks, myKernel, blockSize, 0);
printf("Max active blocks per SM: %d\n", maxActiveBlocks);

블록 크기 선택 가이드라인:

원칙설명
32의 배수Warp 크기에 맞춰 스레드 낭비 방지
128 ~ 512 권장일반적으로 최적 범위
레지스터/Shared Memory 고려자원 사용량이 높으면 블록 크기 줄여야 함
cudaOccupancyMaxPotentialBlockSize 활용자동 최적 계산

7.2 Warp Divergence 최소화

Warp 내 스레드가 서로 다른 분기를 타면 순차 실행이 발생한다.

// 나쁜 예: Warp Divergence 발생
__global__ void divergentKernel(float* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        // threadIdx.x가 짝수/홀수에 따라 분기
        // -> 같은 Warp 내에서 절반만 실행
        if (threadIdx.x % 2 == 0) {
            data[idx] = data[idx] * 2.0f;
        } else {
            data[idx] = data[idx] + 1.0f;
        }
    }
}

// 좋은 예: Warp 단위로 분기
__global__ void convergentKernel(float* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int warpId = threadIdx.x / 32;  // Warp ID로 분기
    if (idx < n) {
        // 같은 Warp 내 모든 스레드가 같은 경로
        if (warpId % 2 == 0) {
            data[idx] = data[idx] * 2.0f;
        } else {
            data[idx] = data[idx] + 1.0f;
        }
    }
}

7.3 Shared Memory Tiling (행렬 곱셈 최적화)

Tiling은 Global Memory 접근 횟수를 줄이는 핵심 기법이다. 4.8절의 matMulTiled에서 이미 구현을 보았으며, 성능 차이를 정리하면 다음과 같다.

방식Global Memory 접근 (MxNxK 행렬)상대 성능
Naive2 * M * N * K1x
Tiled (Shared Memory)2 * M * N * K / TILE_SIZE약 TILE_SIZE 배

TILE_SIZE가 16일 때, Global Memory 접근 횟수가 16배 줄어든다.

7.4 Loop Unrolling

// 수동 Loop Unrolling
__global__ void unrolled(float* data, int n) {
    int idx = blockIdx.x * blockDim.x * 4 + threadIdx.x;

    // 4배 Unrolling
    if (idx < n)         data[idx] *= 2.0f;
    if (idx + 256 < n)   data[idx + 256] *= 2.0f;
    if (idx + 512 < n)   data[idx + 512] *= 2.0f;
    if (idx + 768 < n)   data[idx + 768] *= 2.0f;
}

// 컴파일러 지시자를 이용한 Unrolling
__global__ void pragmaUnrolled(float* A, float* B, float* C, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    float sum = 0.0f;

    #pragma unroll 8
    for (int k = 0; k < N; k++) {
        sum += A[idx * N + k] * B[k];
    }
    C[idx] = sum;
}

7.5 Grid-Stride Loop 패턴

데이터 크기가 총 스레드 수보다 클 때 사용하는 유연한 패턴이다.

__global__ void gridStrideKernel(float* data, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int stride = gridDim.x * blockDim.x;

    // 각 스레드가 stride만큼 건너뛰며 여러 원소 처리
    for (int i = idx; i < n; i += stride) {
        data[i] = data[i] * 2.0f;
    }
}

// 장점:
// 1. 데이터 크기에 관계없이 동작
// 2. 블록/그리드 크기를 자유롭게 조절 가능
// 3. 스레드당 작업량 증가로 커널 시작 오버헤드 감소

7.6 NVIDIA Nsight 프로파일링

# Nsight Compute (커널 레벨 프로파일링)
ncu --set full -o profile_report ./my_cuda_app

# 특정 커널만 프로파일링
ncu --kernel-name myKernel --launch-skip 0 --launch-count 1 ./my_cuda_app

# Nsight Systems (시스템 레벨 프로파일링)
nsys profile --trace=cuda,nvtx -o timeline_report ./my_cuda_app

# 주요 확인 메트릭
# - SM Throughput: SM 활용률
# - Memory Throughput: 메모리 대역폭 활용률
# - Achieved Occupancy: 실제 Occupancy
# - Warp Stall Reasons: Warp 정지 원인
# - L1/L2 Hit Rate: 캐시 적중률

프로파일링 체크리스트:

메트릭목표문제 시 조치
Achieved Occupancy50% 이상블록 크기, 레지스터 사용 조정
Memory Throughput이론 대역폭의 60% 이상Coalesced Access, 캐싱 개선
Compute Throughput이론 연산량의 60% 이상ILP 향상, 불필요 연산 제거
Warp Divergence최소화분기 로직 재구성

8. Python에서 CUDA 다루기

8.1 PyCUDA 기초

PyCUDA는 Python에서 CUDA C 커널을 직접 작성하고 실행할 수 있게 해준다.

import pycuda.autoinit
import pycuda.driver as cuda
from pycuda.compiler import SourceModule
import numpy as np

# CUDA 커널 정의 (C 코드를 문자열로 작성)
mod = SourceModule("""
__global__ void multiply(float *dest, float *a, float *b, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        dest[idx] = a[idx] * b[idx];
    }
}
""")

multiply = mod.get_function("multiply")

# 데이터 준비
n = 1024 * 1024
a = np.random.randn(n).astype(np.float32)
b = np.random.randn(n).astype(np.float32)
dest = np.zeros_like(a)

# 커널 실행
block_size = 256
grid_size = (n + block_size - 1) // block_size

multiply(
    cuda.Out(dest), cuda.In(a), cuda.In(b), np.int32(n),
    block=(block_size, 1, 1), grid=(grid_size, 1)
)

# 결과 검증
assert np.allclose(dest, a * b)
print("PyCUDA multiply: SUCCESS")

8.2 Numba CUDA JIT

Numba는 Python 함수를 CUDA 커널로 JIT 컴파일하는 가장 쉬운 방법이다.

from numba import cuda
import numpy as np
import math

# CUDA 커널 정의
@cuda.jit
def vector_add(a, b, c):
    idx = cuda.grid(1)  # 1D 글로벌 인덱스
    if idx < a.shape[0]:
        c[idx] = a[idx] + b[idx]

# 데이터 준비
n = 1_000_000
a = np.random.randn(n).astype(np.float32)
b = np.random.randn(n).astype(np.float32)
c = np.zeros(n, dtype=np.float32)

# Device로 전송
d_a = cuda.to_device(a)
d_b = cuda.to_device(b)
d_c = cuda.to_device(c)

# 커널 실행
threads_per_block = 256
blocks_per_grid = math.ceil(n / threads_per_block)
vector_add[blocks_per_grid, threads_per_block](d_a, d_b, d_c)

# 결과 가져오기
result = d_c.copy_to_host()
assert np.allclose(result, a + b)
print("Numba CUDA vector_add: SUCCESS")


# 2D 커널 예시: 행렬 곱셈
@cuda.jit
def matmul_kernel(A, B, C):
    row, col = cuda.grid(2)  # 2D 글로벌 인덱스
    if row < C.shape[0] and col < C.shape[1]:
        tmp = 0.0
        for k in range(A.shape[1]):
            tmp += A[row, k] * B[k, col]
        C[row, col] = tmp

# Shared Memory 사용
@cuda.jit
def matmul_shared(A, B, C):
    TILE = 16
    sA = cuda.shared.array(shape=(TILE, TILE), dtype=np.float32)
    sB = cuda.shared.array(shape=(TILE, TILE), dtype=np.float32)

    tx = cuda.threadIdx.x
    ty = cuda.threadIdx.y
    row = cuda.blockIdx.y * TILE + ty
    col = cuda.blockIdx.x * TILE + tx

    tmp = 0.0
    for t in range((A.shape[1] + TILE - 1) // TILE):
        if row < A.shape[0] and (t * TILE + tx) < A.shape[1]:
            sA[ty, tx] = A[row, t * TILE + tx]
        else:
            sA[ty, tx] = 0.0

        if (t * TILE + ty) < B.shape[0] and col < B.shape[1]:
            sB[ty, tx] = B[t * TILE + ty, col]
        else:
            sB[ty, tx] = 0.0

        cuda.syncthreads()
        for k in range(TILE):
            tmp += sA[ty, k] * sB[k, tx]
        cuda.syncthreads()

    if row < C.shape[0] and col < C.shape[1]:
        C[row, col] = tmp

8.3 CuPy (NumPy 대체)

CuPy는 NumPy와 동일한 API를 제공하면서 GPU에서 실행되는 라이브러리이다.

import cupy as cp
import numpy as np
import time

# NumPy와 동일한 API
a_gpu = cp.random.randn(10000, 10000, dtype=cp.float32)
b_gpu = cp.random.randn(10000, 10000, dtype=cp.float32)

# GPU 행렬 곱셈
start = time.time()
c_gpu = cp.dot(a_gpu, b_gpu)
cp.cuda.Stream.null.synchronize()  # GPU 작업 완료 대기
gpu_time = time.time() - start

# CPU 비교
a_cpu = cp.asnumpy(a_gpu)
b_cpu = cp.asnumpy(b_gpu)
start = time.time()
c_cpu = np.dot(a_cpu, b_cpu)
cpu_time = time.time() - start

print(f"GPU: {gpu_time:.4f}s, CPU: {cpu_time:.4f}s")
print(f"Speedup: {cpu_time / gpu_time:.1f}x")

# CuPy에서 커스텀 CUDA 커널 사용
custom_kernel = cp.RawKernel(r'''
extern "C" __global__
void relu_kernel(const float* input, float* output, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        output[idx] = fmaxf(input[idx], 0.0f);
    }
}
''', 'relu_kernel')

n = 1_000_000
x = cp.random.randn(n, dtype=cp.float32)
y = cp.zeros(n, dtype=cp.float32)

block_size = 256
grid_size = (n + block_size - 1) // block_size
custom_kernel((grid_size,), (block_size,), (x, y, n))

# 검증
assert cp.allclose(y, cp.maximum(x, 0))

8.4 PyTorch CUDA 연산

PyTorch에서 CUDA를 활용하는 실전 패턴을 정리한다.

import torch
import torch.nn as nn

# ===== 기본 GPU 사용 =====

# GPU 사용 가능 확인
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA version: {torch.version.cuda}")
print(f"GPU count: {torch.cuda.device_count()}")
print(f"Current device: {torch.cuda.current_device()}")
print(f"GPU name: {torch.cuda.get_device_name(0)}")

# 텐서를 GPU로 이동
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 방법 1: .to(device)
x = torch.randn(1000, 1000)
x_gpu = x.to(device)

# 방법 2: .cuda()
x_gpu = x.cuda()

# 방법 3: 직접 GPU에서 생성
x_gpu = torch.randn(1000, 1000, device='cuda')

# 특정 GPU 지정
x_gpu1 = x.to('cuda:1')  # 두 번째 GPU

# ===== 메모리 관리 =====

# 메모리 사용량 확인
print(torch.cuda.memory_summary())
print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"Cached: {torch.cuda.memory_reserved() / 1e9:.2f} GB")

# 캐시 비우기
torch.cuda.empty_cache()

# 메모리 스냅샷 (디버깅용)
torch.cuda.memory._record_memory_history()
# ... 코드 실행 ...
snapshot = torch.cuda.memory._snapshot()
torch.cuda.memory._dump_snapshot("memory_snapshot.pickle")


# ===== Mixed Precision Training =====
from torch.amp import autocast, GradScaler

model = nn.Linear(1024, 1024).cuda()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
scaler = GradScaler('cuda')

for data, target in dataloader:
    data, target = data.cuda(), target.cuda()
    optimizer.zero_grad()

    # autocas로 FP16 자동 전환
    with autocast('cuda'):
        output = model(data)
        loss = nn.functional.mse_loss(output, target)

    # GradScaler로 FP16 그래디언트 스케일링
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

# Mixed Precision의 이점:
# - 메모리 사용량 약 50% 감소
# - 학습 속도 1.5~3배 향상 (Tensor Core 활용)
# - 정확도 손실 최소

8.5 TensorFlow GPU 설정

import tensorflow as tf

# GPU 확인
print("GPUs available:", tf.config.list_physical_devices('GPU'))

# GPU 메모리 점진적 할당 (권장)
gpus = tf.config.list_physical_devices('GPU')
for gpu in gpus:
    tf.config.experimental.set_memory_growth(gpu, True)

# 또는 메모리 제한 설정
tf.config.set_logical_device_configuration(
    gpus[0],
    [tf.config.LogicalDeviceConfiguration(memory_limit=8192)]  # 8GB
)

# 특정 GPU 사용
with tf.device('/GPU:0'):
    a = tf.random.normal([1000, 1000])
    b = tf.random.normal([1000, 1000])
    c = tf.matmul(a, b)

# Mixed Precision
tf.keras.mixed_precision.set_global_policy('mixed_float16')

8.6 RAPIDS (cuDF, cuML) 소개

RAPIDS는 NVIDIA가 제공하는 GPU 가속 데이터 사이언스 라이브러리 모음이다. 2025년 기준 최신 버전은 26.02이며, cuML은 scikit-learn 대비 5~175배 빠른 성능을 제공한다.

# cuDF: pandas 대체 GPU DataFrame
import cudf

# pandas와 동일한 API
df = cudf.read_csv('large_data.csv')
result = df.groupby('category').agg({'value': 'mean', 'count': 'sum'})
filtered = df[df['value'] > 100]

# pandas와 상호 변환
import pandas as pd
pdf = df.to_pandas()   # cuDF -> pandas
gdf = cudf.from_pandas(pdf)  # pandas -> cuDF


# cuML: scikit-learn 대체 GPU ML
import cuml
from cuml.ensemble import RandomForestClassifier
from cuml.cluster import KMeans

# scikit-learn과 동일한 API
rf = RandomForestClassifier(n_estimators=100, max_depth=16)
rf.fit(X_train, y_train)
predictions = rf.predict(X_test)

# Zero-code-change 가속 (cuml.accel)
# scikit-learn 코드를 수정 없이 GPU로 실행
import cuml.accel
cuml.accel.install()

from sklearn.ensemble import RandomForestClassifier  # 자동으로 GPU 가속!

9. 실전 Hands-on 예제

9.1 예제 1: GPU 벡터 연산 벤치마크 (Numba)

CPU와 GPU의 벡터 연산 성능을 비교하는 실전 예제이다.

from numba import cuda, njit
import numpy as np
import time
import math

# CPU 버전 (Numba JIT)
@njit
def vector_ops_cpu(a, b, c):
    for i in range(a.shape[0]):
        c[i] = math.sqrt(a[i] ** 2 + b[i] ** 2) * math.sin(a[i]) + math.log(abs(b[i]) + 1)

# GPU 버전 (CUDA)
@cuda.jit
def vector_ops_gpu(a, b, c):
    idx = cuda.grid(1)
    if idx < a.shape[0]:
        c[idx] = math.sqrt(a[idx] ** 2 + b[idx] ** 2) * math.sin(a[idx]) + math.log(abs(b[idx]) + 1)

# 벤치마크
sizes = [100_000, 1_000_000, 10_000_000, 100_000_000]

for n in sizes:
    a = np.random.randn(n).astype(np.float32)
    b = np.random.randn(n).astype(np.float32)
    c_cpu = np.zeros(n, dtype=np.float32)
    c_gpu = np.zeros(n, dtype=np.float32)

    # CPU
    start = time.time()
    vector_ops_cpu(a, b, c_cpu)
    cpu_time = time.time() - start

    # GPU
    d_a = cuda.to_device(a)
    d_b = cuda.to_device(b)
    d_c = cuda.to_device(c_gpu)

    threads = 256
    blocks = math.ceil(n / threads)

    # Warm-up
    vector_ops_gpu[blocks, threads](d_a, d_b, d_c)
    cuda.synchronize()

    start = time.time()
    vector_ops_gpu[blocks, threads](d_a, d_b, d_c)
    cuda.synchronize()
    gpu_time = time.time() - start

    c_gpu = d_c.copy_to_host()

    print(f"N={n:>12,}: CPU={cpu_time:.4f}s, GPU={gpu_time:.4f}s, "
          f"Speedup={cpu_time/gpu_time:.1f}x")

9.2 예제 2: 이미지 처리 가속 (CuPy)

GPU를 활용한 실시간 이미지 필터링 예제이다.

import cupy as cp
import numpy as np
from PIL import Image

def gpu_gaussian_blur(image_array, kernel_size=5, sigma=1.0):
    """GPU 가우시안 블러 구현"""
    # 가우시안 커널 생성
    ax = cp.arange(-kernel_size // 2 + 1., kernel_size // 2 + 1.)
    xx, yy = cp.meshgrid(ax, ax)
    kernel = cp.exp(-(xx**2 + yy**2) / (2. * sigma**2))
    kernel = kernel / kernel.sum()

    img_gpu = cp.asarray(image_array, dtype=cp.float32)
    result = cp.zeros_like(img_gpu)

    pad = kernel_size // 2
    # 채널별 처리
    for c in range(img_gpu.shape[2]):
        padded = cp.pad(img_gpu[:, :, c], pad, mode='reflect')
        # 2D Convolution (CuPy FFT 활용)
        from cupyx.scipy.ndimage import convolve
        result[:, :, c] = convolve(img_gpu[:, :, c], kernel)

    return cp.asnumpy(result.clip(0, 255).astype(cp.uint8))


# CuPy RawKernel로 직접 구현한 Sobel Edge Detection
sobel_kernel = cp.RawKernel(r'''
extern "C" __global__
void sobel_filter(const float* input, float* output,
                  int width, int height) {
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
        // Sobel X
        float gx = -1.0f * input[(y-1)*width + (x-1)]
                   + 1.0f * input[(y-1)*width + (x+1)]
                   - 2.0f * input[y*width + (x-1)]
                   + 2.0f * input[y*width + (x+1)]
                   - 1.0f * input[(y+1)*width + (x-1)]
                   + 1.0f * input[(y+1)*width + (x+1)];

        // Sobel Y
        float gy = -1.0f * input[(y-1)*width + (x-1)]
                   - 2.0f * input[(y-1)*width + x]
                   - 1.0f * input[(y-1)*width + (x+1)]
                   + 1.0f * input[(y+1)*width + (x-1)]
                   + 2.0f * input[(y+1)*width + x]
                   + 1.0f * input[(y+1)*width + (x+1)];

        output[y * width + x] = sqrtf(gx * gx + gy * gy);
    }
}
''', 'sobel_filter')

def gpu_sobel_edge_detection(gray_image):
    """GPU Sobel 엣지 검출"""
    h, w = gray_image.shape
    img_gpu = cp.asarray(gray_image, dtype=cp.float32)
    output_gpu = cp.zeros_like(img_gpu)

    block = (16, 16)
    grid = ((w + 15) // 16, (h + 15) // 16)

    sobel_kernel(grid, block, (img_gpu, output_gpu, w, h))

    return cp.asnumpy(output_gpu.clip(0, 255).astype(cp.uint8))

# 사용 예
# img = np.array(Image.open('photo.jpg'))
# blurred = gpu_gaussian_blur(img, kernel_size=11, sigma=3.0)
# gray = np.mean(img, axis=2).astype(np.float32)
# edges = gpu_sobel_edge_detection(gray)

9.3 예제 3: PyTorch 모델 GPU 학습

완전한 PyTorch GPU 학습 파이프라인 예제이다.

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from torch.amp import autocast, GradScaler
import time

# 디바이스 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
if device.type == 'cuda':
    print(f"GPU: {torch.cuda.get_device_name(0)}")

# 모델 정의
class ResidualBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim, dim),
            nn.LayerNorm(dim),
            nn.GELU(),
            nn.Linear(dim, dim),
            nn.LayerNorm(dim),
        )

    def forward(self, x):
        return x + self.net(x)

class DeepModel(nn.Module):
    def __init__(self, input_dim=784, hidden_dim=512, num_classes=10, num_blocks=6):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, hidden_dim)
        self.blocks = nn.Sequential(*[ResidualBlock(hidden_dim) for _ in range(num_blocks)])
        self.head = nn.Linear(hidden_dim, num_classes)

    def forward(self, x):
        x = self.input_proj(x)
        x = self.blocks(x)
        return self.head(x)

# 데이터 생성 (실제로는 데이터셋 로드)
X_train = torch.randn(50000, 784)
y_train = torch.randint(0, 10, (50000,))
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True,
                          num_workers=4, pin_memory=True)  # pin_memory 중요!

# 모델을 GPU로 이동
model = DeepModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scaler = GradScaler('cuda')

# 학습 루프 (Mixed Precision)
def train_epoch(model, loader, criterion, optimizer, scaler, device):
    model.train()
    total_loss = 0
    correct = 0
    total = 0

    for batch_idx, (data, target) in enumerate(loader):
        data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)

        optimizer.zero_grad(set_to_none=True)  # set_to_none=True가 더 효율적

        with autocast('cuda'):
            output = model(data)
            loss = criterion(output, target)

        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

        total_loss += loss.item()
        _, predicted = output.max(1)
        total += target.size(0)
        correct += predicted.eq(target).sum().item()

    return total_loss / len(loader), 100. * correct / total

# 학습 실행
num_epochs = 10
for epoch in range(num_epochs):
    start = time.time()
    loss, acc = train_epoch(model, train_loader, criterion, optimizer, scaler, device)
    elapsed = time.time() - start

    print(f"Epoch {epoch+1}/{num_epochs}: Loss={loss:.4f}, "
          f"Acc={acc:.2f}%, Time={elapsed:.2f}s")

    # GPU 메모리 상태 출력
    if device.type == 'cuda':
        alloc = torch.cuda.memory_allocated() / 1e9
        reserved = torch.cuda.memory_reserved() / 1e9
        print(f"  GPU Memory: {alloc:.2f} GB allocated, {reserved:.2f} GB reserved")

9.4 예제 4: Multi-GPU 학습

DataParallel (간단하지만 비효율적)

import torch
import torch.nn as nn

model = DeepModel().cuda()

# DataParallel 적용 (한 줄로 끝!)
if torch.cuda.device_count() > 1:
    print(f"Using {torch.cuda.device_count()} GPUs with DataParallel")
    model = nn.DataParallel(model)

# 이후 학습 코드는 동일
# 단점: GPU 0에 부하가 집중되는 불균형 문제

DistributedDataParallel (권장)

import torch
import torch.nn as nn
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
import os

def setup(rank, world_size):
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'
    dist.init_process_group("nccl", rank=rank, world_size=world_size)
    torch.cuda.set_device(rank)

def cleanup():
    dist.destroy_process_group()

def train_ddp(rank, world_size):
    setup(rank, world_size)

    # 모델을 해당 GPU에 배치
    model = DeepModel().to(rank)
    ddp_model = DDP(model, device_ids=[rank])

    # 분산 데이터 로더
    train_dataset = TensorDataset(X_train, y_train)
    sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank)
    train_loader = DataLoader(train_dataset, batch_size=256,
                              sampler=sampler, num_workers=4, pin_memory=True)

    optimizer = optim.AdamW(ddp_model.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss()
    scaler = GradScaler('cuda')

    for epoch in range(10):
        sampler.set_epoch(epoch)  # 매 에폭마다 셔플링
        for data, target in train_loader:
            data = data.to(rank, non_blocking=True)
            target = target.to(rank, non_blocking=True)

            optimizer.zero_grad(set_to_none=True)
            with autocast('cuda'):
                output = ddp_model(data)
                loss = criterion(output, target)

            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()

        if rank == 0:
            print(f"Epoch {epoch+1} complete")

    cleanup()

# 실행
# torchrun --nproc_per_node=4 train_ddp.py
import torch.multiprocessing as mp
world_size = torch.cuda.device_count()
mp.spawn(train_ddp, args=(world_size,), nprocs=world_size, join=True)

9.5 예제 5: CUDA 커널 직접 작성 (PyCUDA)

실전에서 유용한 Reduction(합산) 커널을 PyCUDA로 구현한다.

import pycuda.autoinit
import pycuda.driver as cuda
from pycuda.compiler import SourceModule
import numpy as np

# Parallel Reduction 커널
mod = SourceModule("""
__global__ void parallel_reduce(float *input, float *output, int n) {
    extern __shared__ float sdata[];

    unsigned int tid = threadIdx.x;
    unsigned int i = blockIdx.x * blockDim.x * 2 + threadIdx.x;

    // 첫 번째 레벨 리덕션 (글로벌 로드 시 수행)
    float sum = 0.0f;
    if (i < n) sum += input[i];
    if (i + blockDim.x < n) sum += input[i + blockDim.x];
    sdata[tid] = sum;

    __syncthreads();

    // Shared Memory에서 리덕션
    for (unsigned int s = blockDim.x / 2; s > 32; s >>= 1) {
        if (tid < s) {
            sdata[tid] += sdata[tid + s];
        }
        __syncthreads();
    }

    // Warp 내에서는 동기화 불필요 (Warp-level reduction)
    if (tid < 32) {
        volatile float *smem = sdata;
        smem[tid] += smem[tid + 32];
        smem[tid] += smem[tid + 16];
        smem[tid] += smem[tid + 8];
        smem[tid] += smem[tid + 4];
        smem[tid] += smem[tid + 2];
        smem[tid] += smem[tid + 1];
    }

    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}
""")

reduce_kernel = mod.get_function("parallel_reduce")

# 실행
n = 1 << 20  # 1M 원소
data = np.random.randn(n).astype(np.float32)

block_size = 256
grid_size = (n + block_size * 2 - 1) // (block_size * 2)

d_input = cuda.mem_alloc(data.nbytes)
d_output = cuda.mem_alloc(grid_size * 4)  # float32

cuda.memcpy_htod(d_input, data)

reduce_kernel(d_input, d_output, np.int32(n),
              block=(block_size, 1, 1), grid=(grid_size, 1),
              shared=block_size * 4)

# 부분 합계 가져오기
partial_sums = np.zeros(grid_size, dtype=np.float32)
cuda.memcpy_dtoh(partial_sums, d_output)
gpu_sum = partial_sums.sum()

# 검증
cpu_sum = data.sum()
print(f"GPU sum: {gpu_sum:.6f}")
print(f"CPU sum: {cpu_sum:.6f}")
print(f"Difference: {abs(gpu_sum - cpu_sum):.6f}")

10. CUDA 관련 도구 및 라이브러리

10.1 핵심 라이브러리 생태계

라이브러리용도핵심 기능
cuDNN딥러닝Convolution, RNN, Attention, BatchNorm 등 DNN 기본 연산 최적화
cuBLAS선형대수GEMM, TRSM 등 BLAS 레벨 1/2/3 연산. FP8/BF16 Group GEMM 지원 (Blackwell)
NCCLMulti-GPU 통신AllReduce, AllGather, Broadcast 등 집합 통신. NVLink/NVSwitch 최적화
TensorRT추론 최적화그래프 최적화, 양자화 (INT8/FP8), 레이어 퓨전, 동적 배치
TritonGPU 프로그래밍Python DSL로 고성능 커널 작성. OpenAI 개발
CUTLASS커스텀 GEMM템플릿 기반 CUDA 행렬 곱셈 라이브러리
FlashAttentionAttention 가속IO-aware 알고리즘으로 Transformer Attention 가속
cuSPARSE희소 행렬희소 행렬 연산 최적화
cuRAND난수 생성GPU 가속 의사/준난수 생성
cuFFTFFTGPU 가속 고속 푸리에 변환

10.2 cuDNN

# cuDNN은 PyTorch/TensorFlow에 내장되어 자동 사용됨

# PyTorch에서 cuDNN 벤치마크 모드 활성화
import torch
torch.backends.cudnn.benchmark = True   # 최적 알고리즘 자동 선택
torch.backends.cudnn.deterministic = False  # 성능 우선 (재현성 불필요 시)

# cuDNN 버전 확인
print(f"cuDNN version: {torch.backends.cudnn.version()}")
print(f"cuDNN enabled: {torch.backends.cudnn.enabled}")

10.3 NCCL (Multi-GPU 통신)

# PyTorch에서 NCCL 사용 (DistributedDataParallel 내부)
import torch.distributed as dist

# NCCL 백엔드 초기화
dist.init_process_group(backend='nccl')

# 주요 집합 통신 연산
# AllReduce: 모든 GPU의 텐서를 합산하여 각 GPU에 분배
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)

# AllGather: 모든 GPU의 텐서를 모아서 각 GPU에 전체 복사
gathered = [torch.zeros_like(tensor) for _ in range(world_size)]
dist.all_gather(gathered, tensor)

# Broadcast: 한 GPU의 텐서를 모든 GPU에 복사
dist.broadcast(tensor, src=0)

10.4 TensorRT

# TensorRT를 활용한 추론 최적화
import tensorrt as trt
import torch

# PyTorch 모델을 ONNX로 변환
model = MyModel().cuda().eval()
dummy_input = torch.randn(1, 3, 224, 224).cuda()
torch.onnx.export(model, dummy_input, "model.onnx",
                  input_names=['input'], output_names=['output'],
                  dynamic_axes={'input': {0: 'batch_size'}})

# torch_tensorrt를 사용한 간편 변환 (PyTorch 2.x)
import torch_tensorrt

optimized_model = torch_tensorrt.compile(model,
    inputs=[torch_tensorrt.Input(
        min_shape=[1, 3, 224, 224],
        opt_shape=[8, 3, 224, 224],
        max_shape=[32, 3, 224, 224],
        dtype=torch.float16
    )],
    enabled_precisions={torch.float16},
)

# 추론
with torch.no_grad():
    output = optimized_model(input_tensor.half().cuda())

# TensorRT 성능 향상 (일반적):
# - FP32 -> FP16: 2~3배 빠름
# - FP32 -> INT8: 3~5배 빠름
# - 레이어 퓨전 + 최적화: 추가 20~50% 향상

10.5 OpenAI Triton

Triton은 Python으로 고성능 GPU 커널을 작성할 수 있는 프로그래밍 언어이다.

import triton
import triton.language as tl
import torch

# Triton 커널: 벡터 덧셈
@triton.jit
def add_kernel(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
    pid = tl.program_id(axis=0)
    block_start = pid * BLOCK_SIZE
    offsets = block_start + tl.arange(0, BLOCK_SIZE)
    mask = offsets < n_elements

    x = tl.load(x_ptr + offsets, mask=mask)
    y = tl.load(y_ptr + offsets, mask=mask)
    output = x + y

    tl.store(output_ptr + offsets, output, mask=mask)

# 사용
def triton_add(x: torch.Tensor, y: torch.Tensor):
    output = torch.empty_like(x)
    n_elements = output.numel()
    grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']),)
    add_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024)
    return output

# 실행
x = torch.randn(1_000_000, device='cuda')
y = torch.randn(1_000_000, device='cuda')
result = triton_add(x, y)
assert torch.allclose(result, x + y)

# Triton의 장점:
# - CUDA C보다 높은 수준의 추상화
# - 자동 메모리 접근 최적화
# - Python 문법으로 고성능 커널 작성
# - PyTorch 2.0 torch.compile의 백엔드

10.6 FlashAttention

FlashAttention은 Transformer의 Self-Attention 연산을 IO-aware 알고리즘으로 가속한다.

# FlashAttention-3 (H100 최적화)
# 설치: pip install flash-attn

from flash_attn import flash_attn_func

# 입력: (batch, seqlen, nheads, headdim)
q = torch.randn(2, 4096, 32, 128, device='cuda', dtype=torch.float16)
k = torch.randn(2, 4096, 32, 128, device='cuda', dtype=torch.float16)
v = torch.randn(2, 4096, 32, 128, device='cuda', dtype=torch.float16)

# FlashAttention 실행
output = flash_attn_func(q, k, v, causal=True)

# 성능 비교 (FlashAttention-3 vs 표준 Attention):
# - 메모리: O(N^2) -> O(N) (시퀀스 길이 N)
# - 속도: 2~4x 빠름 (H100 FP16 기준 최대 740 TFLOPS)
# - FP8: 최대 1.2 PFLOPS (H100)
# - FlashAttention-3는 FlashAttention-2 대비 1.5~2.0x 추가 향상

# PyTorch 2.0+ 내장 scaled_dot_product_attention
output = torch.nn.functional.scaled_dot_product_attention(
    q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2),
    is_causal=True
)
# PyTorch가 자동으로 FlashAttention 또는 Memory-Efficient Attention 선택

11. CUDA 트러블슈팅

11.1 CUDA OOM (Out of Memory) 해결

GPU 메모리 부족은 가장 흔한 CUDA 문제이다.

# 에러: RuntimeError: CUDA out of memory.

# 해결법 1: 배치 크기 줄이기
# batch_size = 64 -> 32 -> 16

# 해결법 2: Mixed Precision 사용 (메모리 약 50% 절약)
from torch.amp import autocast, GradScaler
scaler = GradScaler('cuda')

# 해결법 3: Gradient Accumulation
accumulation_steps = 4
for i, (data, target) in enumerate(loader):
    with autocast('cuda'):
        loss = model(data) / accumulation_steps
    scaler.scale(loss).backward()

    if (i + 1) % accumulation_steps == 0:
        scaler.step(optimizer)
        scaler.update()
        optimizer.zero_grad()

# 해결법 4: Gradient Checkpointing (메모리-연산 트레이드오프)
from torch.utils.checkpoint import checkpoint
class MemEfficientModel(nn.Module):
    def forward(self, x):
        # 중간 활성화를 저장하지 않고 역전파 시 재계산
        x = checkpoint(self.block1, x, use_reentrant=False)
        x = checkpoint(self.block2, x, use_reentrant=False)
        return x

# 해결법 5: 캐시 비우기
torch.cuda.empty_cache()

# 해결법 6: 메모리 누수 디버깅
print(torch.cuda.memory_summary())

# 해결법 7: 불필요한 텐서 즉시 삭제
del large_tensor
torch.cuda.empty_cache()

# 해결법 8: 추론 시 그래디언트 비활성화
with torch.no_grad():
    output = model(input_data)

11.2 CUDA 드라이버 vs 런타임 버전 불일치

# 문제: "CUDA driver version is insufficient for CUDA runtime version"

# 확인 방법
nvidia-smi           # 드라이버 지원 CUDA 버전 확인
nvcc --version       # 설치된 CUDA Toolkit 버전 확인

# 규칙: 드라이버 CUDA >= Toolkit CUDA 여야 함
# 예: 드라이버가 CUDA 12.2를 지원하면 CUDA 13.0 Toolkit 사용 불가

# 해결법 1: 드라이버 업데이트
sudo apt-get update
sudo apt-get install nvidia-driver-560  # 최신 드라이버 설치

# 해결법 2: 호환되는 CUDA Toolkit으로 다운그레이드
sudo apt-get install cuda-toolkit-12-4

# 해결법 3: PyTorch/TensorFlow에서 맞는 CUDA 빌드 설치
pip install torch --index-url https://download.pytorch.org/whl/cu124

11.3 nvidia-smi 주요 명령어 정리

명령어용도
nvidia-smiGPU 상태 요약
nvidia-smi -l 11초 간격 갱신
nvidia-smi -q상세 정보
nvidia-smi -q -d MEMORY메모리 상세
nvidia-smi -q -d CLOCK클럭 속도
nvidia-smi -q -d TEMPERATURE온도 정보
nvidia-smi -q -d POWER전력 소비
nvidia-smi -q -d PERFORMANCE성능 상태
nvidia-smi --query-gpu=... --format=csvCSV 출력
nvidia-smi pmon -i 0프로세스 모니터링
nvidia-smi dmon -d 1디바이스 모니터링
nvidia-smi topo -mGPU 토폴로지 (NVLink 등)
nvidia-smi -r -i 0GPU 0 리셋
nvidia-smi -pm 1Persistence Mode 활성화
nvidia-smi -pl 300전력 제한 설정 (W)

11.4 cuda-gdb 디버깅

# CUDA 디버그 빌드
nvcc -g -G -O0 -o debug_app app.cu

# cuda-gdb 실행
cuda-gdb ./debug_app

# 주요 명령어
(cuda-gdb) break myKernel            # 커널에 브레이크포인트
(cuda-gdb) run                        # 실행
(cuda-gdb) cuda thread               # 현재 스레드 정보
(cuda-gdb) cuda block                 # 현재 블록 정보
(cuda-gdb) cuda kernel                # 현재 커널 정보
(cuda-gdb) cuda thread (0,0,0)        # 특정 스레드로 전환
(cuda-gdb) cuda block (1,0,0)         # 특정 블록으로 전환
(cuda-gdb) info cuda threads          # 활성 스레드 목록
(cuda-gdb) print threadIdx.x          # 내장 변수 출력
(cuda-gdb) print data[idx]            # 데이터 확인

# compute-sanitizer (메모리 에러 검출)
compute-sanitizer --tool memcheck ./my_app
compute-sanitizer --tool racecheck ./my_app   # 경쟁 조건 검출
compute-sanitizer --tool initcheck ./my_app   # 초기화 검사

11.5 일반적인 에러 메시지와 해결법

에러 메시지원인해결법
CUDA error: out of memoryGPU 메모리 부족배치 크기 줄이기, Mixed Precision, Gradient Checkpointing
CUDA error: device-side assert triggered커널 내 assert 실패 또는 인덱스 초과CUDA_LAUNCH_BLOCKING=1로 실행하여 정확한 위치 확인
CUDA error: an illegal memory access잘못된 메모리 접근compute-sanitizer로 디버깅, 인덱스 범위 확인
CUDA error: no kernel image is availableCompute Capability 불일치올바른 -arch=sm_XX 옵션으로 재컴파일
CUDA driver version is insufficient드라이버 버전 낮음드라이버 업데이트 또는 CUDA Toolkit 다운그레이드
CUDA error: invalid device function잘못된 아키텍처 타겟-gencode 옵션 확인, Fat Binary 사용
cuDNN error: CUDNN_STATUS_NOT_SUPPORTEDcuDNN 버전 불일치 또는 미지원 연산cuDNN 업데이트 또는 입력 형식 확인
NCCL error: unhandled system errorMulti-GPU 통신 실패네트워크 설정, NCCL_DEBUG=INFO 활성화
# 디버깅 팁: 동기 실행 모드로 전환하여 정확한 에러 위치 파악
CUDA_LAUNCH_BLOCKING=1 python train.py

# NCCL 디버깅
NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=ALL python -m torch.distributed.launch train.py

# PyTorch CUDA 메모리 디버깅
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True python train.py

12. CUDA 대안 기술 비교

12.1 주요 GPU 컴퓨팅 프레임워크

기술개발사대상 하드웨어언어생태계 성숙도주요 용도
CUDANVIDIANVIDIA GPUC/C++/Python매우 높음AI/ML, HPC, 과학 컴퓨팅
ROCmAMDAMD GPU (MI300X 등)C/C++ (HIP)중간AI 학습/추론 (PyTorch 지원)
OpenCLKhronos범용 (GPU/CPU/FPGA)C/C++중간크로스 플랫폼 GPU 컴퓨팅
SYCLKhronos범용C++성장 중oneAPI (Intel), 크로스 플랫폼
MetalAppleApple SiliconSwift/Obj-C/C++Apple 생태계macOS/iOS GPU 컴퓨팅
Vulkan ComputeKhronos범용 GPUC/GLSL/SPIR-V중간크로스 플랫폼 GPU 연산
TritonOpenAINVIDIA/AMD GPUPython성장 중고수준 GPU 커널 프로그래밍
WebGPUW3C브라우저 GPUWGSL/JS초기웹 기반 GPU 연산

12.2 CUDA vs ROCm 상세 비교

CUDA 코드:
  cudaMalloc(&d_ptr, size);
  myKernel<<<grid, block>>>(d_ptr);
  cudaDeviceSynchronize();

ROCm HIP 코드 (거의 동일한 API):
  hipMalloc(&d_ptr, size);
  myKernel<<<grid, block>>>(d_ptr);
  hipDeviceSynchronize();

AMD ROCm의 HIP(Heterogeneous-compute Interface for Portability)는 CUDA와 매우 유사한 API를 제공하며, hipify-perl 도구를 사용하면 CUDA 코드를 HIP 코드로 자동 변환할 수 있다. PyTorch는 ROCm을 공식 지원하여 AMD MI300X 등에서 학습이 가능하다.

12.3 선택 가이드

상황권장 기술이유
AI/ML 학습 및 추론CUDA최적화된 라이브러리 생태계 (cuDNN, TensorRT)
AMD GPU 사용ROCm (HIP)CUDA 호환 API, PyTorch 지원
크로스 플랫폼 요구OpenCL 또는 SYCL다양한 하드웨어 지원
Apple 환경MetalmacOS/iOS 유일한 선택지
커스텀 커널 (Python)TritonCUDA C보다 높은 생산성
웹 브라우저 GPUWebGPU표준 웹 API

13. 참고 자료

13.1 공식 문서

  • NVIDIA CUDA Programming Guide: GPU 아키텍처와 프로그래밍 모델의 공식 레퍼런스
  • CUDA C++ Best Practices Guide: 성능 최적화를 위한 모범 사례 모음
  • CUDA Toolkit Release Notes: 각 버전별 변경사항 및 호환성 정보
  • NVIDIA Developer Blog: 최신 기술 동향과 튜토리얼

13.2 주요 참고 링크

자료URL
CUDA Toolkit 다운로드https://developer.nvidia.com/cuda-downloads
CUDA Programming Guidehttps://docs.nvidia.com/cuda/cuda-c-programming-guide/
CUDA Best Practiceshttps://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
cuDNN 문서https://docs.nvidia.com/deeplearning/cudnn/
TensorRT 문서https://docs.nvidia.com/deeplearning/tensorrt/
NCCL 문서https://docs.nvidia.com/deeplearning/nccl/
Nsight Computehttps://developer.nvidia.com/nsight-compute
Nsight Systemshttps://developer.nvidia.com/nsight-systems
RAPIDS 공식 사이트https://rapids.ai/
FlashAttention GitHubhttps://github.com/Dao-AILab/flash-attention
OpenAI Triton GitHubhttps://github.com/openai/triton
PyTorch CUDA 문서https://pytorch.org/docs/stable/cuda.html

13.3 추천 학습 경로

입문자:
  1. nvidia-smi로 GPU 확인
  2. PyTorch에서 .cuda() 사용
  3. Mixed Precision Training 적용
  4. Numba CUDA JIT으로 간단한 커널 작성

중급자:
  5. CUDA C/C++로 커널 직접 작성
  6. Shared Memory, Coalesced Access 최적화
  7. CUDA Stream으로 비동기 실행
  8. Nsight Compute로 프로파일링

고급자:
  9. Triton으로 커스텀 Attention 커널
  10. TensorRT로 추론 최적화
  11. Multi-GPU DDP 학습
  12. CUTLASS로 커스텀 GEMM 구현

마무리

CUDA는 단순한 GPU 프로그래밍 도구를 넘어 현대 AI 인프라의 근간을 이루는 플랫폼이다. Ampere에서 Hopper, 그리고 Blackwell로 이어지는 하드웨어 진화와 함께 CUDA Toolkit도 13.x 시대에 진입하며 CUDA Tile, cuTile DSL 같은 높은 수준의 추상화를 제공하기 시작했다.

실무에서 CUDA를 효과적으로 활용하기 위한 핵심 포인트를 정리하면 다음과 같다.

  1. 기초를 탄탄히: Warp, Block, Grid의 실행 모델과 메모리 계층을 정확히 이해해야 최적화가 가능하다
  2. Python 도구 적극 활용: PyTorch의 Mixed Precision, CuPy, Numba 등으로 대부분의 GPU 가속을 달성할 수 있다
  3. 프로파일링 우선: Nsight Compute/Systems로 병목 지점을 먼저 파악하고 최적화에 투자하라
  4. 메모리가 핵심: Coalesced Access, Shared Memory Tiling, Pinned Memory 등 메모리 최적화가 성능 향상의 80%를 차지한다
  5. 에코시스템 활용: cuDNN, TensorRT, FlashAttention 등 이미 최적화된 라이브러리를 최대한 활용하라

GPU 컴퓨팅의 세계는 계속 진화하고 있다. Triton 같은 고수준 프로그래밍 모델의 등장, FP4/FP6 같은 저정밀도 연산의 확대, 그리고 CUDA Tile로 대표되는 타일 기반 프로그래밍의 도입은 GPU 프로그래밍의 진입 장벽을 낮추면서도 하드웨어 성능을 최대한 끌어낼 수 있는 방향으로 나아가고 있다. 이 가이드가 그 여정의 출발점이 되기를 바란다.