GPU와 CUDA 입문

목표: “CUDA 버전이 안 맞아요”, “nvidia-smi에는 12.4인데 nvcc는 11.8이에요”, “CUDA out of memory 났어요”, “bf16으로 돌리세요”, “DDP로 4장 쓰세요” 같은 말을 들었을 때 어느 층의 이야기인지 구분하고, 직접 확인하고 대처할 수 있기.

터미널·가상환경은 컴퓨터 기본 도구와 개발 환경 입문, 컨테이너 안의 GPU는 Docker 입문, 연구실 서버에서 GPU를 잡고 작업을 올리는 법은 원격 GPU 서버 사용 입문에서 다룬다.


한 줄로 먼저

GPU는 단순한 계산을 수천 개 동시에 하는 장치이고, CUDA는 그 GPU에 일을 시키는 NVIDIA의 소프트웨어 층이다. 딥러닝 코드는 PyTorch → CUDA 라이브러리 → 드라이버 → GPU 순서로 내려가서 실행된다.

내 코드 (train.py)
  ↓ model.to("cuda")
PyTorch            ← pip로 설치. CUDA 런타임·cuDNN·cuBLAS·NCCL을 함께 들고 옴
  ↓
CUDA 라이브러리      ← 행렬곱(cuBLAS), 신경망 연산(cuDNN), GPU 간 통신(NCCL)
  ↓
GPU 드라이버         ← 운영체제에 설치. 하드웨어와 직접 대화
  ↓
GPU 하드웨어         ← 계산 코어 + 자기 전용 메모리(VRAM)

입문자를 가장 많이 괴롭히는 것은 버전 문제와 메모리 문제다. 이 노트는 그 둘을 중심으로 정리한다.


0. 왜 GPU인가: CPU와 GPU

CPU는 똑똑한 일꾼 몇 명이고, GPU는 단순한 일꾼 수천 명이다.

구분CPUGPU
코어 수수 개~수십 개수천 개 (단순한 코어)
코어 하나의 능력강함. 복잡한 분기에 능함약함. 같은 명령을 여러 데이터에 동시에 적용
잘하는 일순서·분기가 많은 일 (OS, 파이썬 인터프리터)같은 계산의 대량 반복 (그래픽, 행렬곱)
설계 목표지연 시간(latency): 한 작업을 빨리처리량(throughput): 많은 작업을 한꺼번에

신경망 계산의 대부분은 행렬곱이다. 행렬곱의 각 칸은 서로 기다릴 필요 없이 따로 계산할 수 있다. 그래서 수천 개 코어에 나눠 주면 그대로 빨라진다. 최근 NVIDIA GPU에는 행렬곱 전용 회로인 Tensor Core도 있어서, 낮은 정밀도(§5)에서 특히 빠르다.

CUDA는 NVIDIA GPU에서 범용 계산을 하게 해 주는 플랫폼이다. GPU에서 실행되는 함수를 커널(kernel) 이라고 부른다(운영체제 커널과는 다른 뜻). PyTorch의 a @ b 한 줄은 내부에서 GPU 커널을 실행시킨다. CUDA는 NVIDIA 전용이라 AMD·Apple GPU에서는 그대로 돌지 않는다(§10).


1. GPU 메모리(VRAM)와 데이터 이동 비용

GPU는 자기 전용 메모리를 가진다. 이것을 VRAM(GPU 메모리)이라고 부른다.

구분RAM (호스트 메모리)VRAM (GPU 메모리)
누가 쓰나CPUGPU
크기 감서버는 수백 GB도 흔함GPU 한 장에 수 GB~백수십 GB
속도(대역폭)상대적으로 느림매우 빠름. 데이터센터 GPU는 HBM(고대역폭 메모리) 사용
늘리기모듈 추가 가능칩에 붙어 있어 못 늘림

GPU는 VRAM에 있는 데이터만 바로 계산한다. 그래서 텐서를 GPU로 옮기는 단계가 필요하다.

x = torch.randn(1024, 1024)    # RAM에 만들어짐 (device='cpu')
x = x.to("cuda")               # PCIe를 건너 VRAM으로 복사
y = x @ x                      # GPU에서 계산, 결과도 VRAM에
print(y.sum().item())          # 결과를 CPU로 가져와 출력

모델 가중치와 입력이 같은 장치에 있어야 한다. 하나라도 CPU에 남아 있으면 Expected all tensors to be on the same device 에러가 난다.

오가는 길이 좁다

CPU와 GPU는 보통 PCIe라는 통로로 연결된다. 이 통로는 GPU 내부 메모리 대역폭보다 훨씬 좁다. 제품마다 다르지만 수십 배 차이로 생각하면 된다.

  • 한 번 GPU에 올린 것은 GPU에서 끝까지 계산한다.
  • 학습 루프 안에서 .cpu(), .item(), print(tensor)를 매 스텝 부르지 않는다. 이 호출은 GPU 작업이 끝날 때까지 CPU를 기다리게(동기화) 만든다.
  • DataLoader에 pin_memory=True를 주고 .to("cuda", non_blocking=True)로 옮기면 복사와 계산을 일부 겹칠 수 있다. pinned memory는 GPU가 직접 읽어 가도록 고정해 둔 RAM 영역이다.

GPU 연산은 비동기다. CPU는 커널을 GPU에 맡기고 바로 다음 줄로 넘어간다. 그래서 시간을 잴 때는 torch.cuda.synchronize()로 GPU가 끝나기를 기다린 뒤 시계를 봐야 한다.


2. 소프트웨어 스택: 각 층의 역할

층무엇어디에확인
GPU 드라이버OS가 GPU를 다루게 하는 프로그램. CUDA 드라이버 API(libcuda) 포함운영체제 (관리자 권한)nvidia-smi
CUDA 툴킷개발 도구 묶음. 컴파일러 nvcc, 헤더, 런타임, 라이브러리보통 /usr/local/cudanvcc --version
CUDA 런타임GPU 메모리 할당·커널 실행용 라이브러리(libcudart)툴킷 안, 또는 PyTorch 휠 안torch.version.cuda
cuBLAS행렬곱 등 선형대수(BLAS) 라이브러리툴킷 또는 휠—
cuDNN합성곱·정규화·어텐션 같은 신경망 연산 라이브러리별도 배포, 또는 휠torch.backends.cudnn.version()
NCCL (“니클”)여러 GPU·서버 사이 통신(all-reduce 등)휠torch.cuda.nccl.version()
PyTorch위 라이브러리를 불러 쓰는 프레임워크가상환경torch.__version__

핵심은 이것이다. 드라이버는 운영체제에 하나만 깔리고, 나머지는 가상환경이나 컨테이너마다 따로 가질 수 있다. Docker 입문의 “드라이버는 호스트, CUDA·PyTorch는 이미지”와 같은 구조다.

PyTorch는 CUDA를 들고 온다

Linux용 PyTorch 휠에는 CUDA 런타임, cuBLAS, cuDNN, NCCL이 함께 들어 있다(최근 버전은 nvidia-* 이름의 pip 패키지로 같이 설치된다). 그래서 PyTorch만 쓸 때는 CUDA 툴킷을 따로 설치하지 않아도 된다. 필요한 것은 충분히 새로운 드라이버뿐이다.

툴킷(nvcc)이 필요한 경우는 직접 CUDA 코드를 컴파일하거나, 설치할 때 CUDA 확장을 빌드하는 패키지를 소스로 설치할 때다.

CUDA 버전을 골라 설치하려면 PyTorch 공식 설치 선택기(Get Started)가 주는 명령을 쓴다.

pip install torch --index-url https://download.pytorch.org/whl/cu121

cu121은 “CUDA 12.1로 빌드된 휠”이라는 뜻이다. 어떤 PyTorch 버전에 어떤 cuXXX 휠이 있는지는 버전마다 다르므로 공식 선택기에서 확인한다.


3. 버전 호환 규칙

드라이버가 상한선을 정한다

드라이버마다 지원하는 최대 CUDA 버전이 있다. 새 드라이버는 옛 CUDA로 만든 프로그램도 돌린다. 반대로 옛 드라이버는 새 CUDA로 만든 프로그램을 못 돌릴 수 있다.

드라이버가 지원하는 최대 CUDA  ≥  PyTorch가 빌드된 CUDA   → 대체로 OK
드라이버가 지원하는 최대 CUDA  <  PyTorch가 빌드된 CUDA   → 문제 가능

같은 메이저 버전(예: 12.x) 안에서는 조금 오래된 드라이버로도 도는 경우가 있다(minor version compatibility). 예외 조건이 있으므로 정확한 최소 드라이버 버전은 NVIDIA CUDA Compatibility 문서와 Release Notes의 표를 본다. 숫자를 외우지 않는다.

세 가지 “CUDA 버전”

$ nvidia-smi
| NVIDIA-SMI 550.xx    Driver Version: 550.xx    CUDA Version: 12.4 |
 
$ nvcc --version
Cuda compilation tools, release 11.8, V11.8.89
표시뜻
nvidia-smi의 CUDA Version이 드라이버가 지원하는 최대 CUDA 버전. CUDA가 설치됐다는 뜻이 아님
nvcc --versionPATH에 잡힌 CUDA 툴킷(컴파일러) 버전. 없으면 command not found
torch.version.cudaPyTorch가 빌드될 때 쓴 CUDA 버전. 실제 실행에 쓰이는 것은 대개 이것

위 예처럼 12.4와 11.8이 달라도 정상이다. nvcc는 PyTorch 실행과 무관하다. nvidia-smi의 나머지 항목을 읽는 법은 원격 GPU 서버 사용 입문에 있다.

새 환경에서 먼저 돌려 볼 코드

import torch
 
print(torch.__version__)               # 예: 2.x.x+cu121
print(torch.version.cuda)              # 빌드 CUDA 버전. CPU 전용 휠이면 None
print(torch.cuda.is_available())       # GPU를 실제로 쓸 수 있는지
print(torch.cuda.device_count())       # 보이는 GPU 수
print(torch.cuda.get_device_name(0))   # 예: NVIDIA A100-SXM4-80GB
print(torch.cuda.get_device_capability(0))  # 예: (8, 0) → §4

torch.cuda.is_available()이 False이면 원인은 대개 셋 중 하나다.

  1. CPU 전용 PyTorch가 깔렸다. torch.version.cuda가 None이다.
  2. 드라이버가 너무 오래됐거나 깨졌다. nvidia-smi가 실패하거나, CUDA Version이 torch.version.cuda보다 낮다.
  3. GPU가 안 보이게 설정됐다. CUDA_VISIBLE_DEVICES가 비었거나, 컨테이너를 --gpus 없이 띄웠다.

CUDA_VISIBLE_DEVICES=2 python train.py로 GPU를 고르면, 그 프로세스 안에서는 2번 GPU가 cuda:0으로 보인다. 번호가 다시 매겨진다.


4. Compute capability: GPU 세대 번호

Compute capability(CC)는 GPU 칩이 어떤 기능을 지원하는지 나타내는 주.부 번호다. CUDA 버전과는 다른 번호다. CUDA 버전은 소프트웨어, CC는 칩의 세대다.

CC아키텍처예
7.0VoltaV100
7.5TuringT4, RTX 20 시리즈
8.0Ampere (데이터센터)A100
8.6Ampere (소비자·워크스테이션)RTX 30 시리즈, RTX A6000
8.9Ada LovelaceRTX 40 시리즈, L40S
9.0HopperH100, H200

이후 세대를 포함한 전체 목록은 NVIDIA CUDA GPUs 페이지에 있다. CC가 중요한 순간은 셋이다.

  • 기능 지원: BF16·TF32 Tensor Core 연산은 Ampere(8.0)부터, FP8은 Ada(8.9)·Hopper(9.0)부터다. V100·T4에서 bf16을 켜면 에러가 나거나 느리게 흉내만 낸다. torch.cuda.is_bf16_supported()로 확인한다.
  • 라이브러리 요구 조건: FlashAttention 같은 최적화 커널은 특정 CC 이상만 지원하는 경우가 많다.
  • 휠의 지원 목록: PyTorch 휠은 정해진 CC 목록에 맞춰 컴파일된다. 목록 밖의 GPU에서는 no kernel image is available for execution on the device 에러가 날 수 있다.

5. 정밀도: 숫자 하나에 몇 바이트를 쓰나

컴퓨터는 실수를 부동소수점(floating point) 으로 저장한다. 비트를 지수(exponent) 와 가수(mantissa) 로 나눈다. 지수 비트가 많으면 표현 범위가 넓고, 가수 비트가 많으면 세밀함이 높다.

형식바이트지수 / 가수특징
FP3248 / 23기본값. 안전하지만 메모리·시간이 많이 듦
TF32(저장은 FP32)8 / 10Ampere 이상 Tensor Core의 계산 모드. 곱셈 내부에서만 정밀도를 낮춤
FP1625 / 10범위가 좁아 작은 기울기가 0이 되기 쉬움. 학습 시 loss scaling 필요
BF1628 / 7FP32와 같은 범위, 정밀도는 낮음. LLM 학습의 사실상 기본
FP81E4M3 / E5M2Ada·Hopper 이상. 별도 라이브러리 지원이 필요한 고급 최적화
INT8 / INT41 / 0.5정수양자화용. 정수 + 배율(scale)로 저장

혼합 정밀도 (mixed precision)

계산은 BF16·FP16으로 빠르게, 가중치 갱신처럼 정확도가 중요한 부분은 FP32로 하는 방식이다. PyTorch에서는 AMP(Automatic Mixed Precision)로 쓴다.

with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
    loss = model(x).loss
loss.backward()
optimizer.step()

FP16을 쓸 때는 작은 기울기가 사라지지 않게 손실을 키웠다 되돌리는 GradScaler를 함께 쓴다(torch.amp.GradScaler("cuda"), 예전 이름 torch.cuda.amp.GradScaler()). TF32는 PyTorch 행렬곱에서 기본으로 꺼져 있고(합성곱을 맡는 cuDNN 쪽은 기본으로 켜져 있다), torch.set_float32_matmul_precision("high")로 켠다. 이 설정 이름은 PyTorch 버전에 따라 바뀔 수 있으니 쓰는 버전의 문서를 확인한다.

양자화

양자화(quantization) 는 학습된 가중치를 8비트·4비트로 줄여 저장하는 기법이다. 주로 추론 메모리를 줄이려고 쓰며, 품질 손실은 모델과 방법에 따라 다르다. 대표 이름은 LLM.int8(), GPTQ, AWQ다. QLoRA는 4비트 기반 모델 위에 LoRA 어댑터만 학습해 미세조정 메모리를 크게 줄였다.

내려받은 모델 파일의 형식(safetensors·GGUF)과 Q4_K_M 같은 양자화 이름을 읽는 법은 로컬 LLM 파일 형식과 정밀도 입문에서 다룬다.


6. 메모리 계산 감 잡기

기본 공식: 파라미터 수 × 바이트

가중치 메모리 ≈ 파라미터 수 × 파라미터 하나의 바이트 수. 7B(70억) 모델이면 이렇다.

정밀도계산가중치 메모리
FP327 × 10⁹ × 4 B28 GB
BF16 / FP167 × 10⁹ × 2 B14 GB
INT87 × 10⁹ × 1 B7 GB
INT47 × 10⁹ × 0.5 B3.5 GB

여기서 GB는 10⁹ 바이트다. nvidia-smi는 MiB(2²⁰ 바이트) 단위라 숫자가 조금 다르게 보인다. 14 × 10⁹ B는 약 13 GiB다.

추론: 가중치 + KV cache + 여유분

추론에는 가중치 외에 KV cache(LLM이 앞 토큰의 key·value를 저장하는 공간), 약간의 activation(계산 중간값), CUDA context(프로세스마다 수백 MB 정도)가 더 든다.

KV cache를 계산해 보자. 가정: 32층, hidden 4096, 헤드마다 K·V를 따로 가짐(GQA 없음, Llama-2-7B 구조), BF16.

토큰 1개당 = 2 (K, V) × 32 (층) × 4096 (hidden) × 2 B = 524,288 B = 0.5 MiB
문맥 4,096 토큰 × 0.5 MiB = 2 GiB   (시퀀스 1개)
배치 8이면 약 16 GiB

KV cache가 가중치(약 13 GiB)보다 커질 수 있다. 여러 헤드가 K·V를 공유하는 GQA를 쓰는 모델은 이 값이 몇 분의 일로 준다. 서빙 엔진이 이 공간을 아끼는 방법은 PagedAttention을 본다.

학습: 몇 배로 불어난다

학습에는 gradient(파라미터마다 기울기), optimizer state(Adam은 파라미터마다 이동 평균 2개를 FP32로), activation(역전파용으로 저장한 순전파 중간값)이 더 필요하다.

자주 인용되는 어림값은 ZeRO 논문의 계산이다. Adam + 혼합 정밀도에서 activation을 빼고 파라미터당 약 16바이트다.

BF16 가중치 2 B + BF16 gradient 2 B
+ FP32 마스터 가중치 4 B + Adam 모멘트 2개 (FP32) 8 B
= 16 B / 파라미터

7B 모델이면 7 × 10⁹ × 16 B = 112 GB에 activation이 더 붙는다. 80 GB GPU 한 장으로 전체 미세조정이 안 되는 이유다. 그래서 여러 GPU에 나눠 담거나(§8), LoRA처럼 학습할 파라미터를 줄인다.

정확한 배수는 조건마다 다르다. 옵티마이저 종류(SGD·8비트 옵티마이저는 상태가 적다), 마스터 가중치 유무, 배치·시퀀스 길이, gradient checkpointing에 따라 크게 바뀐다. 감만 잡고, 실제 값은 직접 잰다.

torch.cuda.reset_peak_memory_stats()
# ... 학습 스텝 한 번 ...
print(torch.cuda.max_memory_allocated() / 2**30, "GiB")

7. OOM 에러: 원인과 대처

torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.00 GiB. ...

OOM(Out Of Memory) 은 VRAM이 모자라 새 텐서를 못 만든다는 뜻이다. 메시지에 요청량과 남은 양이 나오니 끝까지 읽는다.

먼저 확인할 것

  • 다른 프로세스가 GPU를 쓰고 있지 않은가? 남의 작업이나 내가 안 끈 Jupyter 커널이 메모리를 잡고 있을 수 있다. nvidia-smi로 본다.
  • 평가 코드에 torch.no_grad()가 있는가? 없으면 쓰지도 않을 역전파용 activation을 계속 저장한다.
  • 손실을 텐서째 모으고 있지 않은가? total_loss += loss는 계산 그래프를 붙잡아 둔다. loss.item()으로 숫자만 모은다.

대처법

방법줄이는 것대가
배치·시퀀스 길이 줄이기activation처리량 감소
혼합 정밀도 (BF16)activation, 가중치 사본오래된 GPU는 BF16 미지원
gradient accumulation한 번에 올리는 배치시간은 그대로
gradient checkpointingactivation계산 시간 증가
LoRA·양자화gradient, optimizer state, 가중치전체 미세조정과 결과가 다를 수 있음
FSDP·ZeRO로 나누기장당 가중치·gradient·optimizer state통신 비용, 설정 복잡도

gradient accumulation은 작은 배치로 여러 번 역전파해 기울기를 쌓은 뒤 한 번만 갱신한다. 배치 8로 4번 쌓으면 배치 32로 한 번 갱신한 것과 거의 같다(BatchNorm처럼 배치 통계를 쓰는 층은 예외).

accum = 4
for i, batch in enumerate(loader):
    loss = model(**batch).loss / accum   # 평균을 맞추려고 나눔
    loss.backward()                      # 기울기가 계속 더해짐
    if (i + 1) % accum == 0:
        optimizer.step()
        optimizer.zero_grad()

gradient checkpointing은 순전파 중간값을 일부만 저장하고, 역전파 때 필요한 부분을 다시 계산한다. 메모리를 시간으로 바꾸는 셈이다. torch.utils.checkpoint, Hugging Face 모델은 model.gradient_checkpointing_enable()로 켠다.

torch.cuda.empty_cache()의 한계

PyTorch는 캐싱 할당기(caching allocator) 를 쓴다. 텐서를 지워도 메모리를 드라이버에 바로 돌려주지 않고 다음에 쓰려고 쥐고 있다. 그래서 torch.cuda.memory_allocated()(살아 있는 텐서가 쓰는 양)보다 torch.cuda.memory_reserved()(PyTorch가 잡아 둔 양, nvidia-smi에 보이는 쪽)가 크다.

empty_cache()는 쥐고만 있는 빈 캐시를 돌려줘서 다른 프로세스가 쓰게 한다. 하지만 변수가 아직 참조하는 텐서는 못 지우고, 내 프로세스가 쓸 수 있는 총량을 늘려 주지도 않는다. 먼저 del 변수로 참조를 끊어야 한다.

메모리 단편화

남은 총량은 충분한데 OOM이 나기도 한다. 빈 공간이 조각나 있어 큰 텐서를 담을 연속된 공간이 없는 경우로, 단편화(fragmentation) 라고 한다. 에러 메시지에 “reserved but unallocated memory is large”가 보이면 의심한다.

PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True python train.py

할당기 설정 이름과 옵션은 버전에 따라 바뀔 수 있으므로 PyTorch CUDA semantics 문서를 확인한다. torch.cuda.memory_summary()로 할당 상태를 자세히 볼 수 있다.


8. 멀티 GPU 기초

데이터 병렬: 같은 모델, 다른 데이터

GPU마다 모델 전체를 복사하고, 배치를 나눠 계산한 뒤 기울기를 평균 낸다. 모델이 GPU 한 장에 들어갈 때 쓴다.

구분DataParallel (DP)DistributedDataParallel (DDP)
구조프로세스 하나, 스레드 여러 개GPU마다 프로세스 하나
동작매 스텝 GPU 0이 모델을 뿌리고 결과를 모음각자 계산, 기울기만 NCCL로 all-reduce
속도·범위GPU 0에 일이 몰려 느림. 서버 한 대만빠름. 여러 서버까지
권장비권장 (PyTorch 문서도 DDP 권장)표준

all-reduce는 모든 GPU의 값을 합쳐 그 합을 다시 모두에게 나눠 주는 통신이다. DDP는 보통 torchrun으로 띄운다.

torchrun --nproc_per_node=4 train.py   # 이 서버의 GPU 4장에 프로세스 4개

Hugging Face accelerate를 쓰면 이 설정을 대신 해 준다. 유효 배치 = GPU당 배치 × GPU 수 × accumulation 횟수다.

모델이 한 장에 안 들어갈 때

  • FSDP(Fully Sharded Data Parallel): 가중치·gradient·optimizer state를 GPU들에 조각내 나눠 저장하고, 필요할 때만 모아 계산한다.
  • DeepSpeed ZeRO: 같은 아이디어의 Microsoft 라이브러리. Stage 1·2·3으로 무엇까지 나눌지 고른다.
  • 텐서 병렬 / 파이프라인 병렬: 행렬 하나를 쪼개거나, 층을 앞뒤로 나눠 GPU마다 맡긴다.

입문 단계에서는 “먼저 DDP, 안 들어가면 FSDP나 ZeRO” 로 기억하면 충분하다.

  • PCIe: 일반 확장 슬롯 통로. GPU끼리 주고받는 대역폭이 제한된다.
  • NVLink: NVIDIA GPU끼리 직접 잇는 고속 연결. 주로 데이터센터 GPU(A100·H100의 SXM형 등)에 있다. FSDP·텐서 병렬처럼 통신이 많은 방식에서 차이가 크다.

GPU 연결 구조는 nvidia-smi topo -m으로 본다. 같은 8장이라도 연결 방식에 따라 학습 속도가 크게 다르다.


9. GPU 종류 감 잡기

구분예 (VRAM)특징
소비자용 GeForce RTXRTX 3090, RTX 4090 (24 GB)값 대비 성능이 좋지만 VRAM이 작음. RTX 4090은 NVLink 없음
워크스테이션용RTX A6000 (48 GB)VRAM이 크고 장시간 운용용
데이터센터 추론·범용T4 (16 GB), L40S (48 GB)서버 설치, 전력 효율
데이터센터 학습용V100 (16/32 GB), A100 (40/80 GB), H100 (80 GB), H200 (141 GB)HBM, NVLink, ECC(메모리 오류 정정). LLM 학습의 주력

VRAM은 제품 변형(PCIe형·SXM형 등)에 따라 다를 수 있다. 연산 성능(TFLOPS)은 정밀도·희소성 조건마다 공식 숫자가 여러 개이므로 같은 조건끼리 비교한다. 정확한 사양은 NVIDIA 데이터시트를 본다.

“이 모델이 들어가나?”는 VRAM이 정하고, “얼마나 빨리 도나?”는 세대(CC)와 정밀도 지원이 크게 좌우한다.


10. NVIDIA 말고: Mac·AMD·TPU

  • Apple Silicon (MPS): PyTorch는 Mac GPU를 mps 장치로 쓴다(torch.backends.mps.is_available()). CPU와 GPU가 통합 메모리(unified memory) 를 함께 써서 RAM이 큰 Mac은 꽤 큰 모델도 올린다. 대신 CUDA 전용 커널(FlashAttention CUDA 구현, 일부 양자화 라이브러리)은 안 되거나 제한적이다. 미지원 연산은 PYTORCH_ENABLE_MPS_FALLBACK=1로 CPU에 넘길 수 있다. Apple의 MLX도 대안이다.
  • AMD (ROCm): AMD GPU용 스택. ROCm용 PyTorch는 호환을 위해 torch.cuda 이름을 그대로 쓰고, 버전은 torch.version.hip에 나온다. 지원 GPU는 공식 호환 표로 확인한다.
  • TPU: Google의 딥러닝 전용 칩. Google Cloud·Colab에서 JAX나 PyTorch/XLA로 쓴다.

실제로 CUDA 전용 학습 코드를 맥으로 옮겨 보는 사례는 TRM mac에서 학습시켜보기 프로젝트 for ARC-AGI에 있다. M1 Max에서 속도를 재고 어디가 막히는지 정리했다.

Mac에서 짠 코드를 서버로 옮길 때는 장치를 하드코딩하지 않는다.

device = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")

11. 아주 짧은 암기

  • CPU는 소수의 똑똑한 코어, GPU는 수천 개의 단순한 코어. 행렬곱은 GPU 몫.
  • GPU는 VRAM에 있는 것만 계산한다. CPU↔GPU 이동은 비싸다.
  • 드라이버는 OS에 하나, CUDA 런타임·cuDNN은 PyTorch 휠 안에. PyTorch만 쓰면 툴킷 불필요.
  • nvidia-smi의 CUDA Version = 드라이버가 지원하는 최대치. 설치된 버전이 아니다.
  • nvcc = 툴킷 버전, torch.version.cuda = PyTorch 빌드 버전.
  • Compute capability는 칩 세대, CUDA 버전은 소프트웨어. BF16·TF32는 8.0부터.
  • 가중치 메모리 = 파라미터 수 × 바이트. 7B × BF16 ≈ 14 GB.
  • Adam 학습은 activation 빼고 파라미터당 약 16 B. 정확한 배수는 조건마다 다르다.
  • OOM이면: no_grad 확인 → 배치↓ → BF16 → accumulation → checkpointing → LoRA·FSDP.
  • empty_cache()는 살아 있는 텐서를 못 지운다.
  • 멀티 GPU는 DP 말고 DDP. 안 들어가면 FSDP·ZeRO.

입문자가 조심할 점

  • CUDA 툴킷을 습관적으로 설치하지 않는다. PyTorch 휠에 이미 있다. 여러 버전을 깔면 PATH·LD_LIBRARY_PATH가 꼬인다. 공용 서버의 드라이버는 직접 바꾸지 말고 관리자에게 요청한다.
  • 에러가 난 줄이 진짜 원인이 아닐 수 있다. GPU 연산은 비동기라 에러가 나중 줄에서 보고되기도 한다. CUDA_LAUNCH_BLOCKING=1 python train.py로 동기 실행하면 정확한 위치가 나온다(느리므로 디버깅 때만).
  • device-side assert triggered는 대개 인덱스 문제다. 라벨이 클래스 수 이상이거나 토큰 ID가 임베딩 크기를 넘을 때 난다. 같은 코드를 CPU에서 돌리면 읽기 쉬운 에러가 나온다. 이 에러 뒤에는 프로세스를 다시 시작한다.
  • Jupyter 커널은 메모리를 놓지 않는다. 큰 모델을 올렸다면 작업 후 커널을 종료한다. 공용 서버에서 자리를 막는 흔한 원인이다.
  • BF16·TF32 결과는 FP32와 비트 단위로 같지 않다. 실험 기록에 GPU 모델, 드라이버, torch.__version__, torch.version.cuda, 정밀도 설정을 함께 남긴다.
  • Mac에서 돌았다고 서버에서 똑같이 돈다고 가정하지 않는다. MPS와 CUDA는 지원 연산과 메모리 구조가 다르다. 서버에서 작은 설정으로 먼저 돌려 본다.

출처