Published on

PyTorch 모델 4bit 양자화 실패 7가지와 해법

Authors

서빙/파인튜닝 비용을 줄이려고 PyTorch 모델을 4bit로 양자화하면, 이론보다 실전이 훨씬 거칩니다. load_in_4bit=True 한 줄로 끝날 것 같지만, 실제로는 CUDA 커널 호환성, dtype 충돌, 레이어 교체 누락, 메모리 오프로드 설정, 정확도 붕괴 같은 문제로 쉽게 실패합니다.

이 글은 “왜 실패하는지”를 7가지 유형으로 나눠, 증상, 원인, 해결책, 그리고 바로 붙여넣어 확인할 수 있는 코드까지 제공합니다. (LLM 기준 설명이 많지만, 일반 Transformer 계열에도 동일하게 적용됩니다.)

관련해서 양자화 자체의 큰 그림과 추가 트릭은 아래 글도 함께 보면 좋습니다.


0) 시작 전: 내 환경이 4bit를 “지원”하는지 30초 점검

4bit 양자화는 보통 bitsandbytes의 4bit Linear(Linear4bit) 커널을 사용합니다. 여기서 가장 흔한 함정은 “설치는 됐는데 커널이 내 GPU/드라이버 조합에서 동작하지 않는” 경우입니다.

import torch

print("torch:", torch.__version__)
print("cuda available:", torch.cuda.is_available())
if torch.cuda.is_available():
    print("cuda:", torch.version.cuda)
    print("gpu:", torch.cuda.get_device_name(0))

# bitsandbytes 설치 확인
import bitsandbytes as bnb
print("bnb:", bnb.__version__)

추가로, transformers에서 4bit를 쓸 때는 accelerate/transformers/bitsandbytes 버전 조합이 중요합니다. 재현 가능한 환경을 위해 최소한 아래처럼 버전을 고정하는 습관을 권합니다.

pip install -U "transformers==4.39.*" "accelerate==0.27.*" "bitsandbytes==0.43.*"

1) 실패 유형: CUDA 커널 로딩 실패 또는 bitsandbytes가 CPU로 떨어짐

증상

  • 실행 시점에 CUDA Setup failed 류의 에러
  • bitsandbytes가 GPU를 못 잡고 CPU 모드로 동작
  • 특정 GPU(특히 오래된 아키텍처)에서 4bit가 비정상 종료

원인

  • 드라이버/CUDA 런타임/bitsandbytes 빌드 조합 불일치
  • 컨테이너 내 CUDA 라이브러리 누락
  • GPU compute capability가 커널 요구사항과 불일치

해결

  1. 가장 먼저 “컨테이너/호스트에서 CUDA 런타임이 정상인지”를 확인합니다.
nvidia-smi
python -c "import torch; print(torch.cuda.is_available()); print(torch.version.cuda)"
  1. 컨테이너라면 CUDA 런타임이 포함된 베이스 이미지를 쓰고, --gpus all로 실행합니다.

  2. 그래도 안 되면 bitsandbytes를 재설치하거나, 환경에 맞는 휠을 사용해야 합니다. 운영 환경에서 자주 겪는 문제라면, 장애 대응 관점에서 쿠버네티스 레벨 진단 체크리스트도 같이 갖추는 게 좋습니다.


2) 실패 유형: device_map/오프로드 설정 실수로 OOM 또는 성능 폭락

증상

  • 로딩 중 CUDA out of memory
  • 로딩은 되는데 추론이 지나치게 느림(PCIe 왕복, CPU 오프로드 과다)
  • 멀티 GPU에서 한 장에만 몰아 올려 OOM

원인

  • device_map을 적절히 주지 않아 특정 GPU에만 적재
  • max_memory 미설정으로 자동 배치가 비효율
  • CPU offload가 과도하게 걸려 병목 발생

해결

transformers의 자동 샤딩을 제대로 쓰면 대부분 완화됩니다.

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

model_id = "gpt2"  # 예시

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

tokenizer = AutoTokenizer.from_pretrained(model_id)

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
    max_memory={0: "20GiB", "cpu": "64GiB"},
)
  • 단일 GPU라면 device_map="cuda:0"가 오히려 명확합니다.
  • CPU 오프로드가 필요하면 accelerate의 offload 폴더/디스크 속도까지 고려하세요(느린 디스크면 추론이 급격히 느려집니다).

3) 실패 유형: dtype 충돌 (float16/bfloat16/float32)로 NaN, 품질 저하

증상

  • 출력이 갑자기 반복/무의미해짐
  • 로짓이 NaN/inf로 터짐
  • 특정 연산에서 expected scalar type Half but found Float 같은 dtype 에러

원인

  • 4bit 가중치는 양자화되지만, 연산 dtype(compute_dtype)은 별개입니다.
  • Ampere 이상에서는 bfloat16이 안정적인 경우가 많고, 일부 모델은 float16에서 불안정합니다.
  • 레이어노름/소프트맥스 주변에서 dtype 혼합이 터지기 쉽습니다.

해결

  1. 가능하면 bnb_4bit_compute_dtype=torch.bfloat16을 우선 시도합니다.
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)
  1. 입력 텐서 dtype도 모델과 맞춥니다. 추론 시에는 보통 토크나이저 출력은 int64라 상관 없지만, 이미지/오디오 등 float 입력이면 dtype 정합이 중요합니다.

  2. NaN이 난다면 아래처럼 최소 재현으로 어느 지점에서 터지는지 확인합니다.

import torch

torch.autograd.set_detect_anomaly(True)

with torch.no_grad():
    out = model(**tokenizer("test", return_tensors="pt").to(model.device))
    logits = out.logits
    print(torch.isnan(logits).any().item(), torch.isinf(logits).any().item())

4) 실패 유형: 4bit로 “양자화된 줄 알았는데” 실제로는 일부 레이어만 적용

증상

  • VRAM이 기대만큼 줄지 않음
  • print(model)을 보면 Linear가 그대로 남아 있음
  • 성능/메모리 개선이 미미

원인

  • quantization_config가 적용되지 않는 로딩 경로 사용
  • 커스텀 모델 클래스가 transformers의 양자화 후킹을 우회
  • 일부 레이어는 양자화 대상에서 제외되거나(예: lm_head), 자동 교체가 누락

해결

실제로 4bit 레이어로 바뀌었는지 “타입”으로 확인합니다.

import bitsandbytes as bnb

num_4bit = 0
num_linear = 0
for name, module in model.named_modules():
    if module.__class__.__name__ == "Linear4bit":
        num_4bit += 1
    if module.__class__.__name__ == "Linear":
        num_linear += 1

print("Linear4bit:", num_4bit)
print("Linear:", num_linear)
  • Linear가 과도하게 남아 있으면 로딩 코드를 다시 점검해야 합니다.
  • 특정 레이어를 의도적으로 제외하려면(정확도 목적) llm_int8_skip_modules 같은 옵션을 쓰는 방식도 있지만, “모든 게 4bit일 것”이라는 가정은 버리세요.

5) 실패 유형: 학습(파인튜닝) 시 requires_grad/옵티마이저에서 폭발

증상

  • RuntimeError: element 0 of tensors does not require grad
  • 학습은 되는데 loss가 전혀 줄지 않음
  • 옵티마이저 스텝에서 dtype/커널 에러

원인

  • 4bit 양자화 가중치는 일반적인 방식으로 전체 파라미터를 학습하기 어렵습니다.
  • 보통은 LoRA/QLoRA처럼 “저랭크 어댑터만 학습”해야 합니다.
  • 준비 과정(prepare_model_for_kbit_training) 누락 시 레이어노름/임베딩 처리 등이 꼬입니다.

해결

peft를 사용한 QLoRA 패턴으로 전환합니다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    "gpt2",
    device_map="auto",
    quantization_config=bnb_config,
)

model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

핵심은 “4bit는 메모리 절약을 위한 가중치 표현”이고, 학습 전략까지 같이 바꿔야 한다는 점입니다.


6) 실패 유형: 정확도(또는 생성 품질) 급락 — 특히 작은 모델/민감한 태스크

증상

  • 동일 프롬프트에서 답이 짧아지거나 반복이 심해짐
  • 분류/회귀 태스크에서 지표가 급락
  • 특정 도메인(수식/코드/다국어)에서 붕괴

원인

  • 4bit는 손실 압축입니다. 모델/태스크에 따라 손실이 크게 느껴질 수 있습니다.
  • nf4가 대체로 강하지만, compute dtype/더블 양자화/스케일링에 따라 편차가 큽니다.
  • calibration(대표 입력 분포) 없이 무작정 양자화하면 민감한 레이어에서 오차가 커집니다.

해결

  1. 설정 스윕을 “최소 비용”으로 해봅니다.
  • bnb_4bit_quant_type: "nf4" 우선
  • bnb_4bit_compute_dtype: torch.bfloat16 우선
  • bnb_4bit_use_double_quant: 켜고/끄고 비교
  1. 정확도가 중요한 헤드(예: lm_head)나 특정 블록은 FP16/BF16 유지가 더 나을 수 있습니다. 메모리는 조금 늘어도 품질이 크게 회복되는 경우가 있습니다.

  2. 평가를 자동화하세요. 감으로 보면 “좋아 보이는” 착시가 많습니다.

import torch
from torch.nn.functional import cross_entropy

def ppl_on_batch(model, tokenizer, texts, device):
    model.eval()
    losses = []
    with torch.no_grad():
        for t in texts:
            enc = tokenizer(t, return_tensors="pt").to(device)
            out = model(**enc)
            shift_logits = out.logits[:, :-1, :].contiguous()
            shift_labels = enc["input_ids"][:, 1:].contiguous()
            loss = cross_entropy(
                shift_logits.view(-1, shift_logits.size(-1)),
                shift_labels.view(-1),
            )
            losses.append(loss.item())
    return sum(losses) / len(losses)

7) 실패 유형: 배포 환경에서만 터짐 (컨테이너/쿠버네티스/서빙)

증상

  • 로컬에서는 되는데, 배포하면 Illegal instruction/CUDA 에러
  • 파드가 재시작 반복, 로그에만 짧은 에러
  • 특정 노드에서만 실패(드라이버/라이브러리 불일치)

원인

  • 노드별 NVIDIA 드라이버 버전 불일치
  • 이미지에 포함된 CUDA 런타임과 호스트 드라이버의 호환 문제
  • GPU 리소스 할당/권한 문제(nvidia-container-toolkit 설정)

해결

  1. “노드 단위 편차”를 먼저 의심하세요. 같은 이미지인데 노드에 따라 성공/실패가 갈리면 거의 이 케이스입니다.
  2. 쿠버네티스에서는 파드 이벤트/노드 상태/디바이스 플러그인 로그까지 같이 봐야 합니다.
  1. 서빙 레이어(KServe/Knative/Istio)를 얹었다면, 4bit 자체 문제와 인프라 503을 분리해서 봐야 합니다. “모델 로딩 실패로 readiness가 안 뜨는” 상황이 503으로 보이기도 합니다.

실전 체크리스트: 4bit 양자화가 실패할 때의 우선순위

  1. GPU/드라이버/CUDA/bitsandbytes 조합 확인 (nvidia-smi, torch.version.cuda)
  2. device_map="auto"max_memory로 로딩 안정화
  3. bnb_4bit_compute_dtype=torch.bfloat16로 NaN/품질 이슈 완화
  4. 실제로 Linear4bit로 치환됐는지 모듈 타입 검사
  5. 학습이면 QLoRA로 전환 (prepare_model_for_kbit_training + LoRA)
  6. 품질은 자동 평가(PPL/태스크 지표)로 비교
  7. 배포에서만 터지면 노드 편차/드라이버/디바이스 플러그인부터 확인

마무리

4bit 양자화는 “메모리 절약”이라는 큰 보상을 주지만, 그만큼 실패 모드가 다양합니다. 특히 dtype, device_map, 커널 호환성, 그리고 학습 전략(QLoRA)의 4가지만 제대로 잡아도 실패 확률이 크게 내려갑니다.

원하는 형태가 추론 전용인지, 학습까지 포함인지, 그리고 GPU 종류(A100/L4/T4 등)와 배포 환경(로컬, 도커, 쿠버네티스)을 알려주면, 위 7가지 중 어디를 먼저 파야 하는지 우선순위를 더 구체적으로 잡아드릴 수 있습니다.