soobook
GPU

GPU Anatomy

SM, warp, CUDA Core, Tensor Core, VRAM, HBM, nvidia-smi로 GPU 한 장을 읽는 법

GPU는 CPU 옆에 붙은 단순한 계산 보조 장치가 아니다. 수많은 thread를 동시에 실행하는 연산 장치, 자체 memory, cache, command 처리 흐름을 가진 별도의 컴퓨터에 가깝다.

PyTorch나 CUDA 문서를 읽을 때 나오는 SM, warp, CUDA Core, Tensor Core, VRAM, HBM, GPU-Util 같은 단어들에 대해서 알아보자.

GPU Anatomy: CPU host, PCIe boundary, GPU SM cluster, L2 cache, VRAM과 HBM의 관계

CPU vs GPU

CPU는 적은 수의 복잡한 core로 다양한 일을 빠르게 처리한다. branch가 많고, 순서가 복잡하고, latency가 중요한 작업에 강하다.

GPU는 훨씬 많은 thread를 묶어서 같은 종류의 연산을 한꺼번에 처리한다. 딥러닝 학습과 추론에서는 큰 tensor에 비슷한 연산을 반복해서 적용하는 일이 많다. GPU가 강한 영역이다.

다만 GPU가 항상 빠른 것은 아니다. GPU에 맞는 형태로 일이 쪼개져야 하고, 데이터가 GPU memory에 올라가 있어야 한다. CPU가 조금만 처리하면 끝나는 작은 작업은 GPU로 보내는 비용이 더 클 수 있다.

이 그림에서 먼저 기억할 것은 둘이다.

GPU에는 연산을 맡는 여러 SM이 있고, GPU 바깥의 RAM과 별개로 GPU 안의 큰 memory가 있다.

SM

SM은 Streaming Multiprocessor의 약자다. NVIDIA GPU에서 실제 계산이 벌어지는 기본 단위라고 보면 된다.

GPU 하나에는 여러 SM이 들어 있다. CUDA kernel을 실행하면 thread block들이 SM에 배치된다. 하나의 thread block은 한 SM에서 실행되고, SM은 여러 block과 warp를 동시에 올려두고 실행한다.

SM 안에는 여러 실행 유닛과 메모리가 있다.

  • CUDA Core
  • Tensor Core
  • load/store unit
  • special function unit
  • register file
  • shared memory
  • L1 cache

여기서 중요한 점은 CUDA Core 하나를 CPU core 하나처럼 생각하면 안 된다는 것이다.

CPU core는 독립적으로 복잡한 명령 흐름을 처리하는 큰 실행 단위다. CUDA Core는 SM 안에 들어 있는 산술 실행 유닛에 가깝다. CUDA thread 하나가 CUDA Core 하나를 독점한다고 보면 틀린 그림이 된다.

실행의 중심은 CUDA Core 개수보다 SM이 warp를 어떻게 스케줄링하는가에 있다.

Warp

CUDA에서 thread는 혼자 움직이지 않는다. thread block 안의 thread들은 warp라는 묶음으로 실행된다.

단순히 실행 단위라고 생각해도 좋다.

NVIDIA CUDA Programming Guide는 warp를 32개 thread의 묶음으로 설명한다. 이 32개 thread는 SIMT 방식으로 같은 kernel code를 실행한다.

SIMT는 Single Instruction, Multiple Threads의 약자다. 같은 명령을 여러 thread가 각자 다른 데이터에 적용한다는 뜻이다.

예를 들어 vector의 각 원소에 같은 연산을 적용한다고 해보자.

y[i] = x[i] * 2

CPU에서는 loop가 원소를 하나씩 순회하는 그림이 먼저 떠오른다. GPU에서는 많은 thread가 각자 다른 i를 맡고, warp 단위로 묶여 실행된다.

그래서 GPU workload는 “작업 하나가 빠른가”보다 “같은 모양의 작업을 얼마나 많이 동시에 던질 수 있는가”가 중요하다.

Execution Units

CUDA Core는 일반적인 산술 연산을 수행하는 실행 유닛이다.

Tensor Core는 행렬 연산에 특화된 실행 유닛이다. 딥러닝에서 중요한 matrix multiplication, convolution 계열 연산을 더 높은 처리량으로 실행하려고 들어간다.

모델 학습과 추론에서 FP16, BF16, TF32, INT8 같은 precision 이야기가 나오는 이유도 여기에 있다.

어떤 precision을 쓰는지에 따라 Tensor Core를 얼마나 잘 활용할 수 있는지가 달라진다.

CUDA Core와 Tensor Core는 GPU 안의 실행 유닛이다.
CPU core처럼 독립적인 작은 CPU가 여러 개 있는 것이 아니라, SM 안에서 warp 단위 실행을 처리하는 부품으로 봐야 한다.

GPU Memory

GPU는 CPU RAM과 별도의 memory를 가진다. 흔히 VRAM이라고 부르고, CUDA 문서에서는 device memory나 global memory라는 표현도 자주 나온다.

CPU 코드가 CPU RAM을 바로 읽고 쓰듯이, GPU kernel은 GPU memory를 읽고 쓴다.

CPU RAM에 있는 tensor를 GPU가 계산하려면 먼저 GPU memory로 복사해야 한다.

PyTorch 코드로 보면 이런 흐름이다.

import torch

x = torch.randn(1024, 1024)
x_gpu = x.to("cuda")
y_gpu = x_gpu * 2

x는 처음에 CPU RAM에 있다. x.to("cuda")를 호출하면 데이터가 GPU memory로 이동한다. 그 뒤 x_gpu * 2 같은 연산은 GPU 안에서 실행된다.

데이터센터 GPU에서는 이 GPU memory로 HBM을 쓰는 경우가 많다.

HBM은 High Bandwidth Memory의 약자로, GPU 가까이에 배치해 매우 넓은 대역폭을 제공하는 메모리다.

여기서 봐야 할 것은 대역폭이다. GPU는 많은 연산 유닛이 동시에 데이터를 요구한다.

memory가 데이터를 충분히 공급하지 못하면 연산 유닛이 놀게 된다. 이 상태를 흔히 memory-bound라고 부른다.

Memory Hierarchy

GPU memory라고 해서 하나만 있는 것은 아니다. 대략 이런 계층으로 생각하면 된다.

flowchart TD
    REG["Register<br/>thread local"]
    SHARED["Shared Memory<br/>block local"]
    L1["L1 Cache<br/>SM local"]
    L2["L2 Cache<br/>GPU shared"]
    GLOBAL["Global Memory<br/>VRAM / HBM"]

    REG --> SHARED
    SHARED --> L1
    L1 --> L2
    L2 --> GLOBAL

register는 thread가 쓰는 가장 가까운 저장 공간이다. 보통 compiler가 thread local variable을 register에 배치한다.

shared memory는 같은 thread block 안의 thread들이 함께 쓸 수 있는 on-chip memory다. CPU RAM이나 GPU global memory보다 훨씬 가깝다.

L1 cache는 SM 쪽에 있고, L2 cache는 GPU 전체에서 공유된다.

CPU 의 L1/L2 Cache 가 아니다. GPU 의 L1/L2 Cache 다.

global memory는 GPU의 큰 memory다. 용량은 크지만 SM 안의 register나 shared memory보다 멀다.

GPU의 연산 유닛은 많은데, global memory 접근이 비효율적이면 SM이 데이터를 기다린다.

반대로 데이터를 잘 재사용하고 shared memory나 cache를 잘 쓰면 같은 GPU에서도 처리량이 크게 달라진다.

Kernel Launch

CPU가 GPU에 일을 맡길 때는 보통 CUDA kernel을 launch한다.

kernel은 GPU에서 실행되는 함수다. CPU 쪽 프로그램이 kernel launch를 요청하면 CUDA runtime과 driver를 거쳐 GPU command queue에 작업이 들어간다.

sequenceDiagram
    participant APP as User program
    participant CUDA as CUDA runtime
    participant DRV as GPU driver
    participant GPU as GPU
    participant MEM as GPU memory

    APP->>CUDA: kernel launch
    CUDA->>DRV: command 제출
    DRV->>GPU: queue 업데이트
    GPU->>MEM: input 읽기
    GPU->>GPU: SM에서 kernel 실행
    GPU->>MEM: output 쓰기
    GPU->>DRV: completion 알림

이 흐름에서 CPU가 모든 계산을 대신하지 않는다. CPU는 작업을 준비하고, GPU가 이해할 수 있는 형태로 제출한다. 실제 kernel 실행은 GPU의 SM에서 일어난다.

데이터 복사와 kernel 실행은 구분해야 한다.

CPU RAM → GPU memory: 데이터 이동
GPU memory → SM: kernel 실행 중 데이터 읽기
SM → GPU memory: 결과 쓰기
GPU memory → CPU RAM: 필요하면 결과 복사

PyTorch 코드를 볼 때 to("cuda")model(x)는 같은 종류의 일이 아니다. 전자는 주로 데이터 이동이고, 후자는 GPU에서 kernel들이 실행되는 계산 흐름이다.

nvidia-smi

GPU 서버에 들어가면 가장 먼저 보는 도구가 nvidia-smi다.

nvidia-smi는 GPU의 상태를 보여준다. 다만 출력값을 이름만 보고 오해하기 쉽다.

Memory-Usage는 GPU memory가 얼마나 할당되어 있는지 보여준다. 이 값이 높다고 해서 GPU가 계산 중이라는 뜻은 아니다. 큰 모델이 올라가 있으면 계산을 안 해도 memory usage는 높다.

GPU-Util은 GPU가 최근 샘플 구간에서 얼마나 바쁘게 보였는지에 가까운 값이다. 100%라고 해서 모든 SM과 Tensor Core와 memory bandwidth가 완벽하게 쓰였다는 뜻은 아니다.

PowerTemperature는 장치가 어느 정도 부하와 열 상태에 있는지 보는 힌트다. 같은 GPU라도 power limit, thermal limit에 걸리면 clock이 내려갈 수 있다.

Processes 영역은 어떤 process가 GPU memory를 잡고 있는지 보여준다. shared environment에서 GPU memory가 비어 있지 않은 이유를 찾을 때 유용하다.

GPU 상태를 볼 때는 한 숫자만 보지 않는 게 좋다.

Memory-Usage 높음 + GPU-Util 낮음
= 모델이나 tensor는 올라가 있지만 계산은 적을 수 있음

GPU-Util 높음 + 처리량 낮음
= memory bandwidth, CPU data loader, synchronization 같은 다른 병목일 수 있음

Power 낮음 + GPU-Util 낮음
= GPU가 일을 충분히 받지 못하고 있을 수 있음

Host Boundary

GPU를 제대로 보려면 CPU와의 경계도 같이 봐야 한다.

GPU는 혼자 프로그램을 시작하지 않는다. CPU process가 있고, 그 process가 CUDA runtime이나 framework를 통해 GPU에 작업을 넣는다. GPU memory도 process와 driver가 관리한다.

그래서 GPU 성능 문제가 생기면 GPU 안만 봐서는 부족하다.

  • CPU가 데이터를 충분히 빨리 준비하는가
  • CPU RAM에서 GPU memory로 복사가 자주 발생하는가
  • GPU memory에 이미 올라간 tensor를 재사용하는가
  • kernel launch가 너무 잘게 쪼개져 있지는 않은가
  • GPU memory가 부족해서 batch size가 작아지지는 않는가

CPU RAM과 GPU memory 사이의 큰 데이터 이동은 PCIe와 DMA를 타고, GPU 작업 제출은 driver와 command queue를 거친다.

GPU 한 장을 읽는 기준

GPU 한 장을 읽을 때는 세 축을 따로 보면 덜 헷갈린다.

계산 쪽에는 SM이 있다. SM은 warp를 스케줄링하고, CUDA Core와 Tensor Core 같은 실행 유닛으로 연산을 처리한다.

데이터 쪽에는 GPU memory가 있다. VRAM이나 HBM이라고 부르는 영역이고, GPU kernel이 주로 읽고 쓰는 global memory다.

경계 쪽에는 CPU와 이어지는 제어 흐름이 있다. CUDA runtime, driver, command queue, PCIe 데이터 이동이 이 경계에 놓인다.

References