본문 바로가기
Artificial Intelligence/21. PyTorch

[PYTORCH] 텐서 dtype 변경의 3가지 핵심 방법과 실무 해결 가이드 (feat. 16비트 연산)

by Papa Martino V 2026. 4. 5.
728x90

텐서 dtype 변경
텐서 dtype 변경

 

 

딥러닝 프로젝트를 수행하다 보면 연산 속도 저하, 메모리 부족, 혹은 예상치 못한 정밀도 문제로 골머리를 앓는 경우가 많습니다. 이러한 문제들의 상당수는 파이토치(PyTorch)의 가장 기초적인 구성 요소인 **텐서(Tensor)의 데이터 타입(dtype)**을 적절히 관리하지 못해 발생합니다. 특히 대규모 모델 학습이나 Edge 디바이스 배포를 고려할 때 dtype의 선택과 변경은 단순한 코딩 스킬을 넘어 학습 성능과 속도를 결정짓는 핵심적인 엔지니어링 요소입니다. 많은 초보 개발자가 `.to()`나 `.float()` 같은 메서드를 관습적으로 사용하지만, 이들이 내부적으로 어떻게 동작하고 메모리와 성능에 어떤 차이를 만들어내는지 명확히 이해하는 경우는 드뭅니다. 본 글에서는 PyTorch에서 텐서의 dtype을 변경하는 다양한 방법들을 심층 분석하고, 실무에서 마주칠 수 있는 7가지 구체적인 상황과 그 해결책을 개발자가 바로 적용 가능한 코드와 함께 제시합니다. 또한, 최신 트렌드인 **혼합 정밀도 학습(Mixed Precision Training)**과 dtype의 관계까지 다루어, 여러분의 PyTorch 숙련도를 한 단계 끌어올려 드릴 것입니다.

1. PyTorch dtype의 기초와 중요성

dtype 변경 방법을 알아보기 전에, PyTorch가 제공하는 주요 dtype의 특징과 이들이 왜 중요한지 이해해야 합니다.

1.1. 주요 dtypes 비교

파이토치는 다양한 수치 데이터 타입을 지원하며, 각 타입은 표현 가능한 수의 범위와 정밀도, 그리고 차지하는 메모리 크기가 다릅니다.

Data Type (dtype) 설명 메모리 (byte) 정밀도 (비트) 주요 용도
torch.float32 (or torch.float) 단정밀도 부동소수점 4 32 기본 학습 데이터, 가중치 (표준)
torch.float16 (or torch.half) 반정밀도 부동소수점 2 16 메모리 절약, 연산 속도 향상 (Mixed Precision)
torch.bfloat16 Brain 부동소수점 2 16 float16의 대역폭 문제 해결, 수치 안정성 (최신 GPU)
torch.int64 (or torch.long) 부호 있는 64비트 정수 8 64 분류 문제의 라벨(Target), 임베딩 인덱스
torch.uint8 부호 없는 8비트 정수 1 8 이미지 픽셀 값 (0~255)

1.2. dtype 선택이 미치는 영향

  • 메모리 사용량: float32 가중치를 float16으로 변경하면 메모리 사용량이 절반으로 줄어듭니다. 이는 더 큰 배치 크기나 더 큰 모델을 학습할 수 있게 합니다.
  • 연산 속도: 최신 GPU는 16비트 연산(Tensor Cores)을 지원하여 32비트 연산보다 수 배 빠른 속도를 낼 수 있습니다.
  • 수치 정밀도: dtype의 비트 수가 적을수록 소수점 표현의 정밀도가 낮아집니다. 너무 낮은 정밀도는 학습 수렴을 방해하거나 성능 저하를 일으킬 수 있습니다.

2. 텐서 dtype 변경의 3가지 핵심 방법

PyTorch에서 텐서의 dtype을 변경하는 방법은 크게 세 가지로 나눌 수 있습니다.

2.1. `.to()` 메서드 사용 (가장 권장되는 방법)

`.to()` 메서드는 dtype 변경뿐만 아니라 디바이스(CPU/GPU) 간의 텐서 이동도 동시에 처리할 수 있는 가장 범용적이고 명시적인 방법입니다.

import torch

x = torch.randn(2, 3) # 기본 float32
print(f"Original: {x.dtype}")

# 1. dtype만 변경
y = x.to(torch.float16)
print(f"Changed (float16): {y.dtype}")

# 2. dtype과 디바이스를 동시에 변경 (실무 표준)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
z = x.to(device=device, dtype=torch.bfloat16)
print(f"Changed (bfloat16 on {device}): {z.dtype}, {z.device}")

**독창적 팁:** `.to()` 메서드는 원본 텐서가 이미 대상 dtype과 디바이스에 있다면 새로운 복사본을 만들지 않고 원본 텐서를 그대로 반환하여 오버헤드를 줄입니다.

2.2. 전용 캐스팅 메서드 사용 (`.float()`, `.half()`, `.long()` 등)

가장 자주 사용되는 dtype으로의 변경을 위한 간편 메서드들입니다.

x = torch.randint(0, 10, (2, 3)) # 기본 int64

# torch.float32로 캐스팅 (가장 많이 사용)
y_float = x.float() # y_float.to(torch.float32)와 동일

# torch.float16으로 캐스팅
y_half = x.half() # y_half.to(torch.float16)와 동일

# torch.int64로 캐스팅 (분류 문제Target 생성 시)
z_long = torch.tensor([1.2, 3.8]).long() # 값 손실 주의 ([1, 3])

2.3. `.type()` 메서드 사용

문자열로 dtype을 지정할 수 있는 방법입니다. 유연성은 있지만, `.to()`나 전용 메서드에 비해 명시성이 떨어져 실무에서는 덜 권장됩니다.

x = torch.randn(2, 3) # float32

# 문자열을 통한 캐스팅
y = x.type('torch.HalfTensor') # float16
print(f"Changed via .type(): {y.dtype}")

# 다른 텐서의 type을 모방
template = torch.ones(2, 3).long()
z = x.type_as(template) # template와 동일한 long(int64)으로 변경
print(f"Changed via .type_as(): {z.dtype}")

3. 실무 문제 해결을 위한 7가지 Sample Example

개발자가 실무에서 dtype 관리와 관련해 겪는 구체적인 문제들과 그 해결책을 예제 코드로 정리했습니다.

Example 1: CrossEntropyLoss 성능 저하 문제 해결 (Target의 dtype)

많은 분류 문제에서 Target(라벨) 데이터의 dtype이 맞지 않아 오류가 발생하거나 성능이 저하됩니다.

import torch.nn as nn

# 모델 출력 (Logits): 4 클래스 분류, 5개 데이터
outputs = torch.randn(5, 4, requires_grad=True) # float32

# 잘못된 Target (float32로 저장된 경우)
targets_wrong = torch.tensor([0.0, 2.0, 1.0, 3.0, 0.0]) # float32

criterion = nn.CrossEntropyLoss()

# 오류 발생: target must be long, or same dtype as input
try:
    loss = criterion(outputs, targets_wrong)
except Exception as e:
    print(f"Error: {e}")

# [해결방안] Target을 .long()으로 명시적 캐스팅
targets_correct = targets_wrong.long()
loss = criterion(outputs, targets_correct)
print(f"Successfully calculated loss: {loss.item()}")

Example 2: 메모리 부족(OOM) 문제 해결 - Mixed Precision의 수동 구현

대규모 모델 학습 시 GPU 메모리가 부족할 때, 데이터의 정밀도를 낮추는 것이 가장 빠른 해결책입니다.

# 임의의 대형 모델과 데이터
class BigModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(10000, 10000)
    def forward(self, x): return self.fc(x)

# 디바이스 설정
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = BigModel().to(device)
large_input = torch.randn(128, 10000).to(device) # 기본 float32

# OOM 발생 가능성 높음
# output = model(large_input)

# [해결방안] 모델과 데이터의 정밀도를 float16으로 변경
# 1. 모델 가중치를 half()로 변경
model_half = model.half()
# 2. 입력을 half()로 변경
large_input_half = large_input.half()

# 더 적은 메모리로 연산 수행
output_half = model_half(large_input_half)
print(f"Half-precision output dtype: {output_half.dtype}")

**주의:** 수동으로 `half()`를 사용하면 손실 함수 계산 시 정밀도 손실로 인해 학습이 불안정해질 수 있습니다. 실무에서는 PyTorch AMP(Automatic Mixed Precision)를 권장합니다.

Example 3: 이미지 전처리 시 dtype 관리 (`uint8` to `float32`)

이미지 데이터는 주로 `uint8`로 저장되지만, 신경망은 `float32` 입력을 요구합니다. 단순히 캐스팅하면 값이 변하므로 정규화(Normalization)가 필요합니다.

# 임의의 이미지 데이터 (512x512 RGB)
image_uint8 = torch.randint(0, 256, (3, 512, 512), dtype=torch.uint8)
print(f"Original image (uint8): Max={image_uint8.max()}, Min={image_uint8.min()}")

# [잘못된 방법] 단순히 float32로 캐스팅 (값은 0.0~255.0으로 유지)
image_float_wrong = image_uint8.float()

# [해결방안] float32로 캐스팅하면서 0~1 범위로 정규화 (실무 관습)
image_float_correct = image_uint8.float() / 255.0
print(f"Correct image (float32): Max={image_float_correct.max():.4f}, Min={image_float_correct.min():.4f}")

Example 4: `torch.bfloat16`을 이용한 수치 안정성 확보

NVIDIA Ampere 아키텍처 이후의 GPU를 사용할 경우, `float16`의 제한된 표현 범위를 해결하기 위해 `bfloat16`을 사용하는 것이 효과적입니다.

# bfloat16 지원 확인
is_bfloat16_supported = torch.cuda.is_available() and torch.cuda.is_bf16_supported()
print(f"Is bfloat16 supported: {is_bfloat16_supported}")

# 임의의 활성화 값 (매우 큰 값과 작은 값이 혼재)
x = torch.tensor([1e6, 1e-6], device='cuda' if torch.cuda.is_available() else 'cpu')

# float16으로 캐스팅 (큰 값은 overflow(inf), 작은 값은 underflow(0) 발생 가능)
x_half = x.half()
print(f"float16 value: {x_half}") # [inf, 0.0000]

# [해결방안] 지원하는 GPU라면 bfloat16 사용 (표현 범위는 float32와 동일)
if is_bfloat16_supported:
    x_bf16 = x.to(dtype=torch.bfloat16)
    print(f"bfloat16 value: {x_bf16}") # [999424., 9.5367e-07] - 정밀도는 낮지만 inf/zero는 방지

Example 5: 데이터 로더(DataLoader)에서 dtype 사전에 설정하기

학습 루프 내부에서 dtype을 변경하는 것은 오버헤드를 발생시킵니다. 데이터 로더에서 입력을 내보낼 때 대상 dtype으로 변경하는 것이 효율적입니다.

from torch.utils.data import DataLoader, TensorDataset

# 임의의 CPU 데이터
data = torch.randn(1000, 100) # float32
targets = torch.randint(0, 2, (1000,)).float() # float32로 저장된 라벨

# 커스텀 DataLoader를 통해 dtype을 미리 long으로 변경 (Target만)
dataset = TensorDataset(data, targets)

def collate_fn_cast(batch):
    # 각 배치를 가져올 때 dtype을 long으로 명시적 변경
    inputs, targs = zip(*batch)
    inputs = torch.stack(inputs).float() # 입력은 float32 유지
    targs = torch.stack(targs).long() # Target만 long으로 변경
    return inputs, targs

dataloader = DataLoader(dataset, batch_size=32, shuffle=True, collate_fn=collate_fn_cast)

# 학습 루프
for batch_inputs, batch_targets in dataloader:
    # batch_targets는 이미 long 타입
    # 바로 모델 및 손실 함수 연산에 사용 가능
    # loss_fn(model(batch_inputs), batch_targets)
    print(f"Batch Target dtype: {batch_targets.dtype}") # torch.int64
    break

Example 6: NaN(Not a Number) 발생 시 정밀도 격상(Upcasting)을 통한 해결

특정 연산(예: Softmax, Norm)에서 입력 값이 특정 임계치를 넘으면 16비트 정밀도에서는 NaN이 발생할 수 있습니다. 이 부분만 32비트로 연산하여 해결합니다.

# NaN 발생 예제 (매우 큰 로그 확률 값)
x_half = torch.tensor([10.0, -15.0], dtype=torch.float16, device='cuda' if torch.cuda.is_available() else 'cpu')
# exp 연산은 float16에서 65504를 넘으면 inf가 됨
exp_half = torch.exp(x_half)
# [22016., 0.] - 아직 NaN은 아니지만, Softmax에서는 문제가 될 수 있음

# softmax 수동 구현 시 NaN 발생 시뮬레이션
softmax_input_half = torch.tensor([15.0, 5.0, -10.0], dtype=torch.float16, device='cuda' if torch.cuda.is_available() else 'cpu')
# exp(15) is 3,269,017, which overflows float16
exp_inputs = torch.exp(softmax_input_half) # [inf, 148.5, 0.0]

softmax_wrong = exp_inputs / exp_inputs.sum()
print(f"Incorrect Softmax (half): {softmax_wrong}") # [nan, 0.0, 0.0]

# [해결방안] 정밀도 격상(Upcasting)을 통한 NaN 해결
# 1. 32비트로 변경
softmax_input_full = softmax_input_half.float()
# 2. 32비트에서 연산 수행
softmax_correct = nn.functional.softmax(softmax_input_full, dim=0)
print(f"Correct Softmax (cast to full): {softmax_correct}") # [0.9999, 0.0000, 0.0000]

**실무 팁:** PyTorch의 `nn.functional.softmax`는 내부적으로 입력을 `float32`로 캐스팅하여 연산하는 경우도 많지만, 사용자 정의 손실 함수나 민감한 연산에서는 수동 upcasting이 필요합니다.

Example 7: Edge 디바이스 배포를 위한 가중치 양자화(Quantization) - `float32` to `int8`

훈련된 모델을 라즈베리 파이 같은 CPU 기반 Edge 디바이스에 배포할 때, 메모리와 속도를 극대화하기 위해 `int8` dtype으로 양자화합니다.

# 훈련된 임의의 모델
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(50, 50)
    def forward(self, x): return self.fc(x)

model = MyModel().cpu() # 배포 대상 디바이스가 CPU인 경우

# [해결방안] Static Quantization (가장 기본)
import torch.quantization

# 1. 양자화 설정 (디바이스 별 최적 설정)
model.qconfig = torch.quantization.get_default_qconfig('fbgemm') # fbgemm for x86 CPUs

# 2. 양자화 준비 (배포용 모델 생성)
torch.quantization.prepare(model, inplace=True)

# 3. 모델 Calibration (임의의 입력으로 활성화 범위 측정)
calibration_data = torch.randn(10, 50)
model(calibration_data)

# 4. 모델 변환 (가중치를 int8로 변경)
model_quantized = torch.quantization.convert(model, inplace=True)

# 모델의 dtype 변화 확인
# nn.Linear가 nn.quantized.modules.linear.LinearPackedParams로 변환됨
print(f"Quantized model state_dict has elements like: {model_quantized.state_dict()['fc._packed_params._packed_params']}")
# 가중치가 CPU 전용 packed 타입으로 변경되었음을 확인 가능

**결론:** 양자화는 단순히 `.int8()`로 캐스팅하는 것이 아니라, 가중치의 분포를 int8 범위로 스케일링하는 복잡한 과정이며, PyTorch의 전용 라이브러리를 사용해야 합니다.

4. 결론 및 요약

PyTorch에서 텐서의 데이터 타입(dtype)을 관리하는 것은 단순히 데이터의 형식을 맞추는 것을 넘어, 모델의 메모리 효율성, 연산 속도, 수치 안정성을 결정짓는 핵심적인 엔지니어링 작업입니다. 본 글에서 살펴본 것처럼, 가장 권장되는 방법은 `.to()` 메서드를 사용하여 dtype과 디바이스를 명시적으로 동시에 관리하는 것입니다. 또한, 최신 GPU 환경에서는 **Mixed Precision 학습**을 활용하여 `float16`과 `float32`를 적절히 혼합 사용하는 것이 표준이 되었습니다. dtype의 선택은 항상 정밀도와 자원 사용량 간의 트레이드오프(trade-off)를 수반합니다. 실무 개발자는 각 dtype의 특징과 변경 메서드의 동작 원리를 깊이 이해하고, 앞서 제시한 7가지 해결 사례와 같이 상황에 맞는 적절한 dtype 관리 전략을 수립해야 합니다. 이 글이 여러분의 PyTorch 기반 프로젝트를 한 단계 더 최적화하고 수치 문제를 해결하는 데 실질적인 도움이 되기를 바랍니다.

출처:
1. PyTorch 2.3 공식 문서 - 텐서 뷰, 데이터 타입 (`torch.tensor`, `torch.dtype`).
2. PyTorch 공식 튜토리얼 - 혼합 정밀도 학습 (Automatic Mixed Precision).
3. 최신 AI 하드웨어 트렌드와 dtype (bfloat16)에 대한 전문가 분석 보고서.
728x90