Mixed Precision & Device
CUDA, MPS, CPU — PyTorch 디바이스 감지와 혼합 정밀도 학습
이 글은 AI_Finance(ALCHEMETRIC) 프로젝트에서 PyTorch 딥러닝 모델을 다양한 하드웨어에서 실행할 때 마주치는 문제를 다룬다. NVIDIA GPU가 없는 Apple Silicon Mac에서는 어떻게 학습을 돌리는지, Mixed Precision Training은 왜 CUDA에서만 쓰는지, 그리고 디바이스 감지를 자동화한 실제 코드를 살펴본다.
목차
1. 왜 디바이스가 중요한가?
딥러닝 모델은 수백만 개의 숫자를 곱하고 더하는 작업을 수십만 번 반복한다. 이 연산을 어디서(어떤 하드웨어에서) 실행하느냐에 따라 학습 시간이 10분이 될 수도, 10시간이 될 수도 있다.
비유하면, 같은 요리를 만들더라도 가스레인지(CPU)로 하나씩 굽는 것과 대형 오븐(GPU)으로 한꺼번에 굽는 것의 차이다. 딥러닝에서 "디바이스"란, 이 연산을 실행하는 프로세서를 의미한다.
문제: 개발자마다 하드웨어가 다르다
ALCHEMETRIC 프로젝트는 다양한 환경에서 실행될 수 있다.
| 환경 | 하드웨어 | 디바이스 |
|---|---|---|
| 클라우드 서버 | NVIDIA A100 / V100 | cuda |
| 개발용 MacBook | Apple M1 / M2 / M3 | mps |
| 일반 PC / CI 서버 | Intel / AMD CPU | cpu |
코드에 torch.device("cuda")를 하드코딩하면, NVIDIA GPU가 없는 Mac에서는 바로 에러가 난다. 반대로 torch.device("cpu")로 고정하면, 비싼 GPU를 놀게 만드는 셈이다.
2. GPU vs MPS vs CPU — 세 가지 연산 장치
CUDA (NVIDIA GPU)
CUDA는 NVIDIA가 만든 GPU 병렬 컴퓨팅 플랫폼이다. 딥러닝의 사실상 표준이며, PyTorch의 모든 기능이 CUDA를 기준으로 먼저 개발된다. 수천 개의 코어가 행렬 연산을 동시에 처리하므로 CPU 대비 10~100배 빠르다.
CUDA에서만 사용할 수 있는 주요 기능은 다음과 같다.
| 기능 | 설명 | CUDA | MPS | CPU |
|---|---|---|---|---|
| AMP (Mixed Precision) | float16 혼합 학습 | O | X | X |
| Tensor Core | 하드웨어 가속 행렬곱 | O | X | X |
| cuDNN | 최적화된 CNN/RNN 연산 | O | X | X |
| Multi-GPU | 여러 GPU 병렬 학습 | O | X | X |
MPS (Apple Silicon)
MPS(Metal Performance Shaders)는 Apple이 제공하는 GPU 연산 프레임워크다. M1, M2, M3 칩에 내장된 GPU를 PyTorch에서 활용할 수 있게 해 준다. PyTorch 1.12부터 공식 지원이 시작되었다.
CPU보다 2~5배 빠르지만, CUDA만큼 성숙하지는 않다. 특히 float16 autocast가 아직 완전히 지원되지 않아 Mixed Precision을 쓸 수 없다.
CPU (범용 프로세서)
어디서든 작동하는 최후의 보루다. 속도는 가장 느리지만, 별도의 드라이버나 하드웨어 없이 동작한다. CI/CD 파이프라인이나 단순 추론(inference)에서는 CPU로도 충분한 경우가 많다.
3. 디바이스 자동 감지 — _get_device()
ALCHEMETRIC의 dl_trainer.py에는 실행 환경에 맞는 디바이스를 자동으로 선택하는 함수가 있다.
def _get_device() -> torch.device: """사용 가능한 디바이스 자동 감지.""" # 1순위: NVIDIA CUDA GPU if torch.cuda.is_available(): return torch.device("cuda") # 2순위: Apple Silicon MPS if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return torch.device("mps") # 3순위: CPU (항상 사용 가능) return torch.device("cpu")
우선순위: CUDA → MPS → CPU
이 함수의 로직은 단순하지만 의도가 명확하다.
1단계: torch.cuda.is_available() — NVIDIA GPU가 있으면 무조건 CUDA를 쓴다. 가장 빠르고 모든 PyTorch 기능을 지원하기 때문이다.
2단계: torch.backends.mps.is_available() — Apple Silicon Mac이면 MPS를 쓴다. CPU보다 빠르기 때문이다. hasattr 체크는 구버전 PyTorch(1.12 미만)에서 torch.backends.mps 자체가 없을 수 있기 때문에 필요하다.
3단계: 둘 다 없으면 CPU를 사용한다. 어떤 환경에서든 코드가 동작하도록 보장하는 폴백(fallback)이다.
사용 위치 — 학습기 초기화
DeepLearningTrainer 클래스가 생성될 때 디바이스가 결정되고, 이후 모든 학습과 추론에서 이 디바이스를 사용한다.
class DeepLearningTrainer: def __init__(self): SAVED_MODELS_DIR.mkdir(parents=True, exist_ok=True) self.device = _get_device() logger.info( f"DL Trainer 초기화 (device={self.device})", "__init__", )
이렇게 하면 개발자가 직접 디바이스를 지정할 필요 없이, 코드 한 줄 변경 없이 어떤 환경에서든 최적의 장치를 사용하게 된다.
4. Mixed Precision Training — float32와 float16의 혼합
float32 vs float16 — 숫자의 정밀도
컴퓨터에서 소수점이 있는 숫자(실수)를 저장하는 방식에는 여러 종류가 있다.
| 타입 | 비트 수 | 정밀도 | 메모리 | 속도 |
|---|---|---|---|---|
| float32 | 32비트 | 높음 (소수점 7자리) | 4 bytes | 기본 |
| float16 | 16비트 | 낮음 (소수점 3자리) | 2 bytes | 2배 빠름 |
float16은 메모리를 절반만 사용하고 연산도 빠르다. 하지만 정밀도가 낮아서 학습이 불안정해질 수 있다. 특히 gradient(기울기) 값이 매우 작아지면 float16에서는 0으로 반올림되어 학습이 멈추는 underflow 문제가 발생한다.
Mixed Precision — "적재적소에 맞는 정밀도"
Mixed Precision Training (혼합 정밀도 학습)은 두 정밀도를 동시에 사용하는 기법이다.
Forward pass (순전파): float16으로 빠르게 계산한다. 행렬곱 같은 대량 연산은 정밀도를 조금 낮춰도 결과에 큰 차이가 없다.
Backward pass (역전파): float32로 정밀하게 기울기를 계산한다. 기울기 값은 매우 작을 수 있어서 높은 정밀도가 필요하다.
Weight update (가중치 갱신): float32로 유지한다. 모델의 핵심 파라미터이므로 정밀도를 양보하면 안 된다.
AMP란?
AMP(Automatic Mixed Precision)는 PyTorch가 제공하는 Mixed Precision 도구다. 개발자가 직접 float16/float32 변환을 관리하지 않아도, PyTorch가 자동으로 각 연산에 적합한 정밀도를 선택해 준다.
AMP의 두 핵심 구성 요소는 다음과 같다.
| 구성 요소 | 역할 |
|---|---|
| autocast | 연산별로 float16/float32를 자동 선택하는 컨텍스트 매니저 |
| GradScaler | float16에서 발생하는 gradient underflow를 방지하기 위해 loss를 스케일링 |
5. AMP 학습 루프 — autocast + GradScaler
이제 ALCHEMETRIC 프로젝트의 실제 학습 루프 코드를 살펴보자. 핵심은 디바이스에 따라 AMP를 켜고 끄는 분기다.
AMP 사용 여부 결정
# CUDA에서만 AMP 활성화 use_amp = self.device.type == "cuda" amp_scaler = torch.amp.GradScaler() if use_amp else None
use_amp는 불리언 플래그로, 디바이스가 CUDA일 때만 True가 된다. MPS나 CPU에서는 False이므로 AMP를 사용하지 않는다.
텐서를 디바이스로 이동
for X_batch, y_batch in train_loader: X_batch = X_batch.to(self.device) # 텐서를 GPU/MPS/CPU로 이동 y_batch = y_batch.to(self.device) # 라벨도 같은 디바이스로 optimizer.zero_grad()
.to(device)는 텐서를 특정 디바이스의 메모리로 복사한다. 모델과 데이터가 같은 디바이스에 있어야 연산이 가능하다. 다른 디바이스에 있으면 PyTorch가 런타임 에러를 발생시킨다.
CUDA 경로 — AMP 활성화
if use_amp: # Step 1: autocast 블록 안에서 forward pass (float16) with torch.amp.autocast(device_type="cuda"): logits = model(X_batch) loss = criterion(logits, y_batch) # Step 2: loss를 스케일링한 뒤 backward (gradient underflow 방지) amp_scaler.scale(loss).backward() # Step 3: 스케일링을 되돌린 뒤 gradient clipping amp_scaler.unscale_(optimizer) nn.utils.clip_grad_norm_(model.parameters(), grad_clip) # Step 4: 옵티마이저 step + scaler 업데이트 amp_scaler.step(optimizer) amp_scaler.update()
각 단계를 하나씩 살펴보자.
Step 1 — autocast: torch.amp.autocast 블록 안에서는 행렬곱 등 대량 연산이 자동으로 float16으로 실행된다. BatchNorm 같이 정밀도가 중요한 연산은 float32를 유지한다.
Step 2 — scale + backward: GradScaler가 loss 값을 크게 늘린 뒤 역전파한다. 이렇게 하면 아주 작은 gradient도 float16에서 0이 되지 않는다.
Step 3 — unscale + clip: 옵티마이저에 전달하기 전에, 스케일링을 원래대로 되돌린다. 그 뒤 gradient clipping으로 기울기 폭발을 방지한다.
Step 4 — step + update: 스케일된 gradient로 가중치를 갱신하고, scaler의 스케일 팩터를 다음 iteration에 맞게 업데이트한다.
MPS / CPU 경로 — 일반 학습
else: # float32 기본 정밀도로 학습 logits = model(X_batch) loss = criterion(logits, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), grad_clip) optimizer.step()
MPS와 CPU에서는 autocast와 GradScaler 없이 float32만으로 학습한다. 코드가 훨씬 단순하고, 정밀도 문제가 발생하지 않는다.
추론(Inference) 시 디바이스 관리
@torch.no_grad() def _evaluate(self, model: nn.Module, loader: DataLoader) -> dict: model.eval() for X_batch, y_batch in loader: X_batch = X_batch.to(self.device) # 학습과 같은 디바이스 logits = model(X_batch) probs = torch.softmax(logits, dim=1) preds = logits.argmax(dim=1) # 결과를 CPU로 가져와서 numpy 변환 all_preds.extend(preds.cpu().numpy())
추론 시에는 @torch.no_grad() 데코레이터로 gradient 계산을 비활성화한다. 학습할 필요가 없으므로 메모리와 연산을 절약할 수 있다. 결과 텐서는 .cpu().numpy()로 CPU에 가져와서 scikit-learn 메트릭 계산에 사용한다.
6. MPS의 현실 — Apple Silicon에서의 제약
Apple Silicon의 MPS 백엔드는 Mac에서 딥러닝을 돌릴 수 있게 해준다는 점에서 획기적이다. 하지만 CUDA와 동일한 수준을 기대하면 안 된다.
MPS가 좋은 점
1) 별도 GPU 없이 가속: MacBook 한 대로 개발과 학습을 모두 할 수 있다. NVIDIA GPU가 없어도 CPU보다 빠르게 프로토타이핑이 가능하다.
2) 통합 메모리 아키텍처: Apple Silicon은 CPU와 GPU가 같은 메모리를 공유한다. .to(device) 시 데이터 복사 오버헤드가 CUDA보다 적다.
3) 전력 효율: GPU 서버 대비 전력 소모가 현저히 낮다. 금융 데이터처럼 비교적 작은 모델에서는 충분한 성능을 낸다.
MPS의 한계
| 한계 | 설명 | 프로젝트 영향 |
|---|---|---|
| float16 autocast 미지원 | MPS에서 autocast 사용 시 일부 연산에서 에러 발생 | AMP를 CUDA에서만 활성화 |
| 일부 연산 미구현 | 특정 PyTorch 연산이 MPS에서 지원되지 않음 | CPU 폴백이 필요한 경우 존재 |
| 수치 정밀도 차이 | CUDA 대비 미세한 결과 차이 발생 가능 | 금융 모델에서 재현성 검증 필요 |
| 디버깅 도구 부족 | CUDA의 nvprof, nsight 같은 프로파일링 도구 없음 | 성능 병목 분석이 어려움 |
왜 AMP를 MPS에서 쓰지 않는가?
가장 핵심적인 이유는 MPS 백엔드의 float16 연산이 아직 불완전하기 때문이다. torch.amp.autocast(device_type="mps")를 사용하면, 일부 레이어에서 지원하지 않는 연산을 만나 런타임 에러가 발생할 수 있다.
또한 MPS GPU에는 NVIDIA의 Tensor Core가 없다. Tensor Core는 float16 행렬곱을 하드웨어 수준에서 가속하는 전용 유닛인데, 이것이 없으면 float16을 써도 속도 이점이 크지 않다.
# 이 코드가 AMP를 CUDA에서만 쓰는 이유 use_amp = self.device.type == "cuda" # MPS에서 autocast를 강제로 사용하면? # with torch.amp.autocast(device_type="mps"): # 일부 연산에서 에러! # logits = model(X_batch) # RuntimeError: MPS does not support ...
따라서 우리 프로젝트에서는 안정성을 우선으로, MPS에서는 float32 기본 정밀도로 학습한다. Apple이 MPS의 float16 지원을 개선하면 향후 활성화할 수 있도록, 디바이스 체크 한 줄로 분기를 관리하고 있다.
7. 프로젝트 적용 정리
ALCHEMETRIC 프로젝트에서 디바이스와 Mixed Precision이 어떻게 통합되어 있는지 한눈에 정리하면 다음과 같다.
전체 흐름
# 1. 디바이스 감지 (앱 시작 시 1회) device = _get_device() # cuda / mps / cpu # 2. 모델을 디바이스로 이동 model = model.to(device) # 3. AMP 결정 (CUDA에서만) use_amp = device.type == "cuda" amp_scaler = torch.amp.GradScaler() if use_amp else None # 4. 학습 루프 for X_batch, y_batch in train_loader: X_batch = X_batch.to(device) # 데이터 이동 y_batch = y_batch.to(device) if use_amp: # CUDA: autocast + GradScaler ... else: # MPS/CPU: float32 기본 학습 ... # 5. 추론 with torch.no_grad(): preds = model(X_batch.to(device)) result = preds.cpu().numpy() # CPU로 복귀
디바이스별 동작 요약
| 항목 | CUDA | MPS | CPU |
|---|---|---|---|
| 디바이스 감지 | torch.cuda.is_available() | torch.backends.mps.is_available() | 기본 폴백 |
| AMP | 활성화 | 비활성화 | 비활성화 |
| 정밀도 | float16 + float32 혼합 | float32 | float32 |
| GradScaler | 사용 | None | None |
| 상대 속도 | 10~100x | 2~5x | 1x (기준) |
| 추론 시 | torch.no_grad() | torch.no_grad() | torch.no_grad() |
설계 원칙
1) 단일 진입점: _get_device() 함수 하나로 디바이스 감지를 중앙 관리한다. 새로운 가속기가 추가되면 이 함수만 수정하면 된다.
2) 안전한 분기: AMP를 CUDA에서만 활성화하여, MPS/CPU에서의 런타임 에러를 원천 차단한다.
3) 일관된 텐서 관리: 모든 텐서와 모델이 self.device를 통해 같은 디바이스에 배치된다. 디바이스 불일치 에러를 방지한다.
4) 추론 시 CPU 복귀: .cpu().numpy() 패턴으로, GPU/MPS 결과를 scikit-learn 등 CPU 기반 라이브러리에 안전하게 전달한다.
'Project > 🚀 AI Finance 2026' 카테고리의 다른 글
| #29 [모듈4-5] RL 에이전트의 행동을 결정하는 보상 함수 설계 (0) | 2026.03.19 |
|---|---|
| #28 [모듈4-4] AI 에이전트가 스스로 매매 전략을 학습하는 법 (0) | 2026.03.19 |
| #26 [모듈4-2] DL Early Stopping & Overfitting (0) | 2026.03.18 |
| #25 [모듈4-1] PyTorch & Deep Learning (0) | 2026.03.18 |
| #24 [모듈4] Deep Learning (0) | 2026.03.18 |