양자화 개념

양자화 개념 (Quantization concepts)

양자화(quantization)는 Transformers 라이브러리에 있는 것 같은 대규모 머신러닝 모델의 메모리 사용량과 계산 비용을 줄여줍니다. 표준적인 32비트 부동소수점(float32) 대신 8비트 정수(int8) 같은 더 낮은 정밀도 데이터 타입으로 모델의 가중치와 활성화를 표현해서 이렇게 해요.

출처: 문서

본문

모델의 정밀도를 낮추면 몇 가지 중요한 이점이 있습니다.

  • 모델 크기 축소: 낮은 정밀도 데이터 타입은 저장 공간을 덜 차지합니다. 예를 들어 int8 모델은 float32 모델보다 대략 4배 더 작아요.
  • 더 빠른 추론: 낮은 정밀도 데이터 타입, 특히 정수에 대한 연산은 호환되는 하드웨어에서 훨씬 빨라질 수 있습니다(CPU와 GPU는 흔히 int8 연산을 위한 특수 명령어를 가져요). 이는 낮은 지연 시간으로 이어집니다.
  • 에너지 소비 감소: 더 빠른 계산과 더 작은 메모리 전송은 보통 더 낮은 전력 사용으로 이어져요.

양자화의 주된 트레이드오프는 *효율(efficiency)*과 정확도(accuracy) 사이입니다. 정밀도를 줄이면 자원이 절약되지만 작은 오류(양자화 노이즈)가 필연적으로 발생해요. 목표는 적절한 방식(affine/symmetric), 세분화(granularity, per-tensor/channel), 기법(PTQ/QAT)을 사용해 이 오류를 최소화해서, 모델이 대상 작업에서 성능이 최대한 덜 저하되게 하는 것입니다.

아래 섹션에서 양자화 방식(schemes), 세분화, 기법을 다룹니다.

양자화 방식 (Quantization schemes)

핵심 아이디어는 원래 float32 가중치와 활성화에서 발견되는 값의 범위를 int8이 표현하는 훨씬 작은 범위(보통 $[-128, 127]$)로 매핑하는 것입니다.

이 섹션에서는 몇 가지 양자화 기법이 어떻게 동작하는지 다룹니다.

Affine 양자화

가장 흔한 방법은 affine 양자화입니다. 주어진 float32 텐서(레이어의 가중치 같은)에 대해 최솟값 $val_{min}$과 최댓값 $val_{max}$을 찾습니다. 이 범위 $[val_{min}, val_{max}]$는 int8 범위 $[q_{min}, q_{max}]$, 보통 $[-128, 127]$에 매핑됩니다.

이 매핑을 수행하는 두 가지 주요 방식, symmetric(대칭)과 asymmetric(비대칭)이 있습니다. symmetric과 asymmetric 양자화 사이의 선택은 float32 범위가 int8 범위에 어떻게 매핑되는지를 결정해요.

  • Symmetric(대칭): 이 방식은 원래 float32 범위가 0을 중심으로 대칭이라고 가정합니다($[ -a, a ]$). 이 범위는 예를 들어 $[-127, 127]$처럼 int8 범위에 대칭적으로 매핑돼요. 핵심 특징은 float32 값 $0.0$이 int8 값 $0$에 직접 매핑된다는 것입니다. 매핑을 정의하는 데 **스케일(scale, $S$)**이라는 단 하나의 매개변수만 필요해요. 계산을 단순화할 수 있지만, 원래 데이터 분포가 자연스럽게 0을 중심으로 하지 않으면 정확도가 떨어질 수 있어요.

  • Asymmetric (Affine)(비대칭): 이 방식은 데이터가 0을 중심으로 한다고 가정하지 않습니다. float32의 정확한 범위 $[val_{min}, val_{max}]$을 $[-128, 127]$ 같은 전체 int8 범위에 매핑해요. **스케일(scale, $S$)**과 제로 포인트(zero-point, $Z$) 두 매개변수가 필요합니다.

    스케일(scale, $S$): float32 범위와 int8 범위 사이의 비율을 나타내는 양의 float32 숫자입니다.

$$ S = \frac{val_{max} - val_{min}}{q_{max} - q_{min}} $$

제로 포인트(zero-point, $Z$): float32 값 $0.0$에 해당하는 int8 값입니다.

$$ Z = q_{min} - round\left(\frac{val_{min}}{S}\right) $$

[!TIP] symmetric 양자화에서 $Z$는 보통 0으로 고정됩니다.

이 매개변수들을 사용하면 float32 값 $x$를 아래 공식으로 int8($q$)에 양자화할 수 있어요.

$$ q = round\left(\frac{x}{S} + Z\right) $$

int8 값 $q$는 아래 공식으로 근사 float32로 역양자화(dequantize)할 수 있습니다.

$$ x \approx S \cdot (q - Z) $$

추론 중에는 행렬 곱 같은 계산을 int8 값($q$)으로 수행하고, 그 결과를 (흔히 내부적으로 int32 같은 더 높은 정밀도의 누적 타입을 사용해서) float32로 역양자화한 뒤 다음 레이어에 전달합니다.

int4와 가중치 패킹 (weight packing)

int4 양자화는 모델 크기와 메모리 사용을 더 줄입니다(int8과 비교해 절반). 앞서 본 것과 같은 affine 또는 symmetric 양자화 원칙이 적용되며, float32 범위를 int4가 표현 가능한 16개 값(부호 있는 int4의 경우 $[-8, 7]$)에 매핑해요.

int4 양자화의 핵심 측면은 **가중치 패킹(weight packing)**입니다. 대부분의 하드웨어는 메모리에서 4비트 데이터 타입을 기본적으로 처리할 수 없으므로, 두 개의 int4 값을 저장과 전송을 위해 단일 int8 바이트에 함께 패킹하는 것이 일반적입니다. 예를 들어 첫 번째 값은 바이트의 하위 4비트, 두 번째 값은 상위 4비트를 차지할 수 있어요 (packed_byte = (val1 & 0x0F) | (val2 << 4)).

int4는 기본 int4 계산이 없어도 여전히 유용합니다. 주요 이점이 메모리 대역폭 감소에서 오기 때문이에요. 메모리(RAM 또는 VRAM)에서 계산 유닛으로 패킹된 int4 가중치(int8로 저장됨)를 로딩하는 것은 int8 가중치를 로딩하는 것보다 두 배 빠릅니다. 큰 모델에서는 메모리 접근이 흔히 중요한 병목입니다. 더 빠른 데이터 전송으로 인한 속도 향상은 그 자리에서 풀고 역양자화하는 계산 오버헤드를 능가할 수 있어서, 특히 메모리 바운드(memory-bound) 시나리오에서 전반적으로 더 빠른 추론을 만들어냅니다.

하지만 int4 양자화는 보통 int8보다 더 큰 정확도 저하를 만듭니다. int4에서 좋은 성능을 내려면 GPTQ나 AWQ 같은 고급 양자화 기법이 필요한 경우가 많아요.

FP8 양자화 (A8W8)

더 새로운 데이터 타입인 8비트 부동소수점(FP8)은 정밀도를 줄이면서도 특정 시나리오에서 int8보다 더 많은 정확도를 유지할 수 있는 또 다른 방법을 제공합니다. FP8은 부동소수점 구조(부호, 지수, 가수)를 유지하지만 더 적은 비트를 사용합니다.

두 가지 흔한 FP8 변형이 있습니다.

  • E4M3: 부호 1비트, 지수 4비트, 가수 3비트. 더 높은 정밀도(가수 비트가 더 많음)를 제공하지만 더 작은 동적 범위(지수 비트가 더 적음)를 가져요.
  • E5M2: 부호 1비트, 지수 5비트, 가수 2비트. 더 넓은 동적 범위를 제공하지만 더 낮은 정밀도를 갖습니다.

FP8은 활성화(A)와 가중치(W)를 모두 8비트 정밀도로 양자화하는 A8W8 양자화 방식에 사용됩니다.

int8이 넓은 지원을 받는 반면, 효율적인 FP8 계산은 NVIDIA H100/H200/B100, AMD Instinct MI300 시리즈, Intel XPU 같은 최신 GPU에서 찾을 수 있는 특정 하드웨어 기능을 요구합니다. 기본 하드웨어 가속이 없으면 FP8의 이점이 완전히 실현되지 않을 수 있어요.

Transformers는 FBGEMM, FineGrainedFP8, compressed-tensors 같은 특정 백엔드를 통해 FP8을 지원합니다. 이 백엔드들은 적절한 하드웨어와 설정이 사용될 때 밑바닥의 FP8 변환과 계산을 처리합니다.

세분화 (Granularity)

양자화 매개변수($S$와 $Z$)는 다음 두 가지 방법 중 하나로 계산할 수 있어요.

  • Per-Tensor(텐서별): 텐서 전체에 대해 한 세트의 $S$와 $Z$. 더 간단하지만 데이터 값이 텐서 안에서 크게 다양하면 덜 정확합니다.
  • Per-Channel(채널별, 또는 Per-Group/Block): 각 채널 또는 그룹에 대해 별도의 $S$와 $Z$. 약간 더 많은 복잡성과 메모리를 대가로 더 정확하고 성능이 좋아요.

양자화 기법 (Quantization techniques)

두 가지 주요 양자화 기법 유형이 있습니다.

  • 훈련 후 양자화(Post-Training Quantization, PTQ): 모델이 완전히 훈련된 후에 양자화를 적용합니다.
  • 양자화 인지 훈련(Quantization-Aware Training, QAT): 훈련 중에 양자화의 반올림 오류를 시뮬레이션하는 "가짜 양자화(fake quantization)" 연산을 삽입해 양자화 효과를 시뮬레이션합니다. 이를 통해 모델이 양자화에 적응할 수 있고, 특히 낮은 비트 폭에서 보통 더 나은 정확도를 얻을 수 있어요.

Transformers에서의 양자화 (Quantization in Transformers)

Transformers는 bitsandbytes, torchao, compressed-tensors 등 여러 양자화 백엔드를 통합합니다(더 많은 백엔드는 양자화 개요를 참고하세요).

모든 백엔드는 HfQuantizer API와 관련 QuantizationConfig 클래스 아래에 통합됩니다. Contribution 가이드에 나와 있듯이 커스텀 HfQuantizer와 QuantizationConfig를 구현해 자신만의 커스텀 양자화 백엔드를 통합할 수 있어요.

Transformers에서 양자화의 일반적인 워크플로우는 다음과 같습니다.

  1. 하드웨어와 사용 사례에 적합한 양자화 방법을 선택합니다 (Overview 또는 Selecting a quantization method 가이드가 도움이 돼요).
  2. Hugging Face Hub에서 사전 양자화된 모델을 로드하거나, float32/float16/bfloat16 모델을 로드하고 QuantizationConfig로 특정 양자화 방법을 적용합니다.

아래 예시는 8B 파라미터 모델을 로드해 bitsandbytes로 4비트에 양자화하는 것입니다.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_id = "meta-llama/Llama-3.1-8B-Instruct"

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

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=quantization_config,
    dtype=torch.bfloat16,
    device_map="auto"
)

더 알아보기 (Learn more)

양자화와 관련 성능 최적화 개념을 더 깊이 탐구하려면 아래 자료를 확인해 보세요.