Bitsandbytes

Bitsandbytes (8-bit / 4-bit 양자화)

제한된 컴퓨팅 자원으로 대형 모델을 다루는 방법 중 가장 손쉽게 시작할 수 있는 게 bitsandbytes 라이브러리예요. 이 라이브러리는 하드웨어 가속기 함수를 감싼 가벼운 Python 래퍼로 LLM용 양자화 도구를 제공하며, 모델의 메모리 사용량을 줄여줍니다. 이 가이드에서는 Transformers에서 bitsandbytes로 8-bit(LLM.int8)와 4-bit(QLoRA) 양자화를 쓰는 법을 정리해요.

출처: Bitsandbytes - Hugging Face Transformers 공식문서

bitsandbytes가 제공하는 것

핵심적으로 bitsandbytes는 다음을 제공합니다.

  • 양자화된 선형 레이어: 표준 PyTorch 선형 레이어를 메모리 효율적인 양자화 대체품인 Linear8bitLt, Linear4bit 레이어로 대체
  • 최적화된 옵티마이저: optim 모듈을 통한 흔한 옵티마이저의 8-bit 버전으로, 메모리 요구량을 줄인 대형 모델 훈련 가능
  • 행렬 곱셈: 양자화 포맷을 활용하는 최적화된 행렬 곱셈 연산

bitsandbytes의 주요 양자화 기능은 두 가지입니다.

  1. LLM.int8() — 성능 저하 없이 추론을 더 접근 가능하게 만드는 8-bit 양자화 방법. 순진한 양자화와 달리 LLM.int8()은 중요한 계산에 대해 더 높은 정밀도를 동적으로 유지해서 민감한 부분의 정보 손실을 막아요.
  2. QLoRA — 작은 학습 가능한 저랭크 적응(LoRA) 가중치를 삽입해 모델을 더 압축하면서도 훈련 가능성을 유지하는 4-bit 양자화 기법.

Note: 손쉬운 양자화 경험을 위해 bitsandbytes 커뮤니티 Space를 쓸 수도 있어요.

아래 명령으로 bitsandbytes를 설치합니다.

pip install --upgrade transformers accelerate bitsandbytes

소스에서 컴파일하려면 bitsandbytes 설치 가이드를 따르세요.

하드웨어 호환성

bitsandbytes는 CUDA 11.8-13.0의 NVIDIA GPU, Intel XPU, Intel Gaudi(HPU), CPU에서 지원됩니다. 추가 플랫폼 지원을 위한 노력이 진행 중이며, 의견·테스트에 관심이 있다면 bitsandbytes 저장소를 참고하세요.

NVIDIA GPU (CUDA)

Linux x86-64, Linux aarch64, Windows 플랫폼에서 지원됩니다.

기능 최소 하드웨어 요구사항
8-bit 옵티마이저 NVIDIA Pascal (GTX 10X0 시리즈, P100) 이상 *
LLM.int8() NVIDIA Turing (RTX 20X0 시리즈, T4) 이상
NF4/FP4 양자화 NVIDIA Pascal (GTX 10X0 시리즈, P100) 이상 *

Intel GPU (XPU)

Linux x86-64와 Windows x86-64 플랫폼에서 지원됩니다.

Intel Gaudi (HPU)

Gaudi2·Gaudi3에 대해 Linux x86-64에서 지원됩니다.

CPU

Linux x86-64, Linux aarch64, Windows x86-64 플랫폼에서 지원됩니다.

양자화 예시

BitsAndBytesConfigfrom_pretrained()에 넘겨 모델을 양자화합니다. Accelerate를 지원하고 torch.nn.Linear 레이어를 포함하는 모델이라면 어떤 모달리티에서도 동작합니다.

모델을 8-bit로 양자화하면 메모리 사용량이 절반으로 줄고, 대형 모델에서는 device_map="auto"를 설정해 사용 가능한 모든 GPU에 가중치를 효율적으로 분산하세요.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_8bit=True)

model_8bit = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-1b7", 
    device_map="auto",
    quantization_config=quantization_config
)

기본적으로 torch.nn.LayerNorm 같은 다른 모든 모듈은 기본 torch dtype으로 설정됩니다. 이 모듈들의 데이터 타입은 dtype 파라미터로 바꿀 수 있어요. dtype="auto"로 설정하면 모델의 config.json 파일에 정의된 데이터 타입으로 로딩됩니다.

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_8bit=True)

model_8bit = AutoModelForCausalLM.from_pretrained(
    "facebook/opt-350m", 
    device_map="auto",
    quantization_config=quantization_config, 
    dtype="auto"
)
model_8bit.model.decoder.layers[-1].final_layer_norm.weight.dtype

8-bit로 양자화한 후에는 Transformers와 bitsandbytes의 최신 버전을 쓰지 않는 한 양자화된 가중치를 Hub로 푸시할 수 없어요. 최신 버전이라면 push_to_hub()로 8-bit 모델을 Hub에 올릴 수 있습니다. 양자화 config.json 파일이 먼저 푸시되고, 이어서 양자화된 모델 가중치가 푸시됩니다.

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_8bit=True)

model = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-560m", 
    device_map="auto",
    quantization_config=quantization_config
)

model.push_to_hub("bloom-560m-8bit")

모델을 4-bit로 양자화하면 메모리 사용량이 4배 줄고, 대형 모델에서는 device_map="auto"로 가중치를 GPU들에 분산하세요.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_4bit=True)

model_4bit = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-1b7",
    device_map="auto",
    quantization_config=quantization_config
)

기본적으로 torch.nn.LayerNorm 같은 다른 모듈은 torch.float16으로 변환됩니다. dtype 파라미터로 바꿀 수 있고, dtype="auto"config.json에 정의된 타입으로 로딩합니다.

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_4bit=True)

model_4bit = AutoModelForCausalLM.from_pretrained(
    "facebook/opt-350m",
    device_map="auto",
    quantization_config=quantization_config, 
    dtype="auto"
)
model_4bit.model.decoder.layers[-1].final_layer_norm.weight.dtype

4-bit 모델을 직렬화해 push_to_hub()로 Hub에 올리려면 최신 bitsandbytes 버전이 필요해요. 로컬 저장에는 save_pretrained()를 사용합니다.

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_4bit=True)

model = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-560m", 
    device_map="auto",
    quantization_config=quantization_config
)

model.push_to_hub("bloom-560m-4bit")

[!WARNING] 8 및 4-bit 훈련은 추가 파라미터 훈련만 지원됩니다.

get_memory_footprint로 메모리 사용량을 확인하세요.

print(model.get_memory_footprint())

quantization_config 없이 from_pretrained()로 양자화 모델을 로딩합니다.

from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("{your_username}/bloom-560m-8bit", device_map="auto")

LLM.int8

이 절에서는 8-bit 양자화의 구체적 기능(오프로딩, 이상치 임계값, 모듈 변환 스킵, 파인튜닝)을 살펴봅니다.

오프로딩

8-bit 모델은 매우 큰 모델을 메모리에 맞추기 위해 가중치를 CPU와 GPU 사이에서 오프로드할 수 있어요. CPU로 보내진 가중치는 float32로 저장되며 8-bit로 변환되지 않습니다. 예를 들어 bigscience/bloom-1b7의 오프로딩을 BitsAndBytesConfig로 켭니다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(llm_int8_enable_fp32_cpu_offload=True)

lm_head만 CPU로 보내고 나머지는 전부 GPU에 맞는 커스텀 디바이스 맵을 설계해요.

device_map = {
    "transformer.word_embeddings": 0,
    "transformer.word_embeddings_layernorm": 0,
    "lm_head": "cpu",
    "transformer.h": 0,
    "transformer.ln_f": 0,
}

이제 커스텀 device_mapquantization_config로 모델을 로딩합니다.

model_8bit = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-1b7",
    dtype="auto",
    device_map=device_map,
    quantization_config=quantization_config,
)

이상치 임계값

"이상치(outlier)"는 특정 임계값보다 큰 hidden state 값으로, fp16으로 계산됩니다. 값은 보통 정규분포([-3.5, 3.5])를 띠지만 대형 모델에서는 분포가 매우 달라질 수 있어요([-60, 6] 또는 [6, 60]). 8-bit 양자화는 값이 ~5일 때 잘 동작하지만 그 이상이면 성능 저하가 두드러집니다. 좋은 기본 임계값은 6이지만, 더 불안정한 모델(작은 모델이나 파인튜닝)에서는 더 낮은 임계값이 필요할 수 있어요.

최적 임계값을 찾으려면 BitsAndBytesConfigllm_int8_threshold 파라미터로 실험하세요. 예를 들어 임계값을 0.0으로 설정하면 어느 정도 정확도 손실을 대가로 추론이 크게 빨라집니다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig

model_id = "bigscience/bloom-1b7"

quantization_config = BitsAndBytesConfig(
    llm_int8_threshold=0.0,
    llm_int8_enable_fp32_cpu_offload=True
)

model_8bit = AutoModelForCausalLM.from_pretrained(
    model_id,
    dtype="auto",
    device_map=device_map,
    quantization_config=quantization_config,
)

모듈 변환 스킵

모든 모듈을 8-bit로 양자화하면 불안정할 수 있어서, 일부 모듈은 전체 정밀도로 두는 게 좋습니다. 그 이름을 BitsAndBytesConfigllm_int8_skip_modules 파라미터에 넘기세요. BLOOM에서는 입력 임베딩과 가중치를 공유하는 lm_head를 스킵합니다.

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_id = "bigscience/bloom-1b7"

quantization_config = BitsAndBytesConfig(
    llm_int8_skip_modules=["lm_head"],
)

model_8bit = AutoModelForCausalLM.from_pretrained(
    model_id,
    dtype="auto",
    device_map="auto",
    quantization_config=quantization_config,
)

파인튜닝

PEFT 라이브러리는 flan-t5-large, facebook/opt-6.7b 같은 대형 모델의 8-bit 양자화 파인튜닝을 지원합니다. 훈련에는 device_map 파라미터를 넘길 필요가 없어요. 자동으로 모델을 GPU에 로딩하기 때문입니다. 다만 device_map 파라미터로 디바이스 맵을 커스터마이즈할 수는 있고, device_map="auto"는 추론에만 써야 합니다.

QLoRA

이 절에서는 4-bit 양자화의 구체적 기능(연산 데이터 타입 변경, Normal Float 4(NF4), 중첩 양자화)을 살펴봅니다.

연산 데이터 타입

BitsAndBytesConfig에서 기본값 float32에서 bf16으로 데이터 타입을 바꾸면 연산이 빨라집니다.

import torch
from transformers import BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)

Normal Float 4 (NF4)

NF4는 QLoRA 논문의 4-bit 데이터 타입으로, 정규분포에서 초기화된 가중치에 맞게 적응됐어요. 4-bit 기본 모델 훈련에는 NF4를 사용해야 합니다.

from transformers import BitsAndBytesConfig

nf4_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
)

model_nf4 = AutoModelForCausalLM.from_pretrained(model_id, dtype="auto", quantization_config=nf4_config)

추론에서 bnb_4bit_quant_type은 성능에 큰 영향을 주지 않아요. 다만 모델 가중치와 일관성을 유지하려면 bnb_4bit_compute_dtypedtype 값을 사용해야 합니다.

중첩 양자화

중첩 양자화(nested quantization)는 성능 저하 없이 추가 메모리를 아낄 수 있어요. 이미 양자화된 가중치를 두 번째로 양자화해서 파라미터당 추가 0.4비트를 절약합니다. 예를 들어 중첩 양자화로 Llama-13b 모델을 16GB NVIDIA T4 GPU에서 시퀀스 길이 1024, 배치 크기 1, gradient accumulation 4스텝으로 파인튜닝할 수 있어요.

from transformers import BitsAndBytesConfig

double_quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
)

model_double_quant = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-13b-chat-hf", dtype="auto", quantization_config=double_quant_config)

bitsandbytes 모델 디양자화

양자화된 후에는 dequantize()로 모델을 원래 정밀도로 되돌릴 수 있지만, 어느 정도 품질 손실이 있을 수 있어요. 디양자화된 모델을 담을 충분한 GPU 메모리가 있는지 확인하세요.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("facebook/opt-125m", BitsAndBytesConfig(load_in_4bit=True))
model.dequantize()

리소스

8-bit 양자화에 대한 자세한 내용은 A Gentle Introduction to 8-bit Matrix Multiplication for transformers at scale using Hugging Face Transformers, Accelerate and bitsandbytes 블로그에서 배울 수 있어요.

4-bit 양자화는 이 노트북으로 시도하고, 자세한 내용은 Making LLMs even more accessible with bitsandbytes, 4-bit quantization and QLoRA에서 확인하세요.

더 알아보기 (Learn more)