Quantization Granularity

마지막 수정:

quantizationgranularityint4int8group-quantizationcalibration

Quantization에서 bit-width만큼 중요한 질문이 있다.

scale 하나를 몇 개의 값이 공유할 것인가?

이 질문을 granularity라고 부른다.

Per-tensor

1 scale / tensor

simple baseline

Per-channel

1 scale / output channel

INT8 weights

Per-token

1 scale / token activation

LLM activations

Group-128

1 scale / 128 weights

INT4 weights

Block

1 scale / 2D tile

specialized kernels
Granularity asks how many real values share one scale. Finer granularity usually preserves accuracy, but it adds scale metadata and can make kernels more complex.

Coarse scale은 단순하지만 outlier에 약하다

가장 단순한 방식은 tensor 전체가 scale 하나를 공유하는 것이다.

per-tensor:
W 전체 -> scale 1개

하지만 tensor 안에 큰 outlier가 하나라도 있으면 scale은 그 값을 보호하도록 커진다. 그러면 작은 값들은 integer grid의 일부만 쓰게 된다.

결과적으로 memory는 줄었지만 실제 resolution은 많이 잃는다.

큰 값 1개를 위해 scale을 키움
-> 대부분의 작은 값이 0 근처 code에 몰림
-> quantization error 증가

Weight는 per-channel이 기본 출발점이다

LLM의 linear layer weight는 보통 [out_features, in_features] 모양이다. Per-channel quantization은 output channel마다 scale을 하나씩 둔다.

per-channel:
각 output row -> scale 1개

이 방식은 channel마다 weight range가 다를 때 효과적이다. 책의 예시처럼 transformer projection weight에서는 많은 channel이 global max보다 훨씬 작은 range를 쓰기 때문에, per-tensor scale을 쓰면 integer range를 크게 낭비한다.

Per-channel의 비용은 scale metadata다. 하지만 큰 transformer layer에서는 weight 수에 비해 scale 수가 작아서 overhead가 보통 매우 작다.

그래서 INT8 weight quantization에서는 per-channel이 자연스러운 기본값이다.

Activation은 per-token이 중요해진다

Activation은 weight와 다르다. Weight는 학습 후 고정되지만 activation은 input마다 바뀐다.

같은 layer
다른 prompt
다른 activation range

Transformer에서는 token마다 activation magnitude도 다를 수 있다. 한 token이 큰 값을 만들면 per-tensor scale은 그 token에 맞춰지고, 나머지 token들은 resolution을 잃는다.

그래서 LLM activation quantization에서는 dynamic per-token scale이 자주 등장한다.

per-token:
[batch, seq_len, hidden]에서 token마다 scale 1개

이는 더 정확하지만 runtime에 scale을 계산하고 들고 다녀야 한다.

INT4 weight는 group granularity가 필요하다

INT8에는 256개 code가 있다. 하지만 INT4에는 16개 code만 있다.

같은 channel 안에서 수천 개 weight가 16개 code를 공유하면 너무 거칠다. 그래서 INT4 weight quantization에서는 channel보다 더 작은 묶음이 필요하다.

group quantization:
input dimension을 따라 G개 weight마다 scale 1개
대표 기본값: group size 128

Group size가 작아질수록 scale이 더 많아져서 accuracy는 좋아질 수 있다. 대신 scale metadata가 늘고, kernel은 quantized weight와 scale을 더 자주 함께 읽어야 한다.

책의 핵심 판단은 이렇다.

INT8 weights -> per-channel
INT4 weights -> group-wise, often group-128

Memory layout도 granularity의 일부다

Group을 아무 축으로나 자르면 안 된다.

GPU kernel은 연속된 memory를 읽을 때 효율이 좋다. PyTorch의 linear weight가 row-major라면 input dimension 쪽 값들이 연속되어 있다. 그래서 group quantization도 보통 input dimension을 따라 자른다.

좋은 layout:
연속된 G개 weight
-> scale 1개
-> vectorized load/dequant

반대로 group이 memory에서 흩어져 있으면 scale은 정확해져도 kernel 효율이 나빠질 수 있다.

INT4에서는 packing도 중요하다. 4-bit 값 두 개가 byte 하나에 들어가기 때문에, group size와 kernel tile이 byte boundary에 잘 맞아야 한다.

Block은 더 특수한 2D granularity다

Block quantization은 1D group을 넘어 2D tile마다 scale을 두는 방식이다.

block:
matrix tile 하나 -> scale 1개

LLM inference weight quantization에서는 1D group 방식이 더 흔하다. Block 방식은 특정 hardware format이나 training 쪽에서 더 중요해질 수 있다.

이 path에서는 먼저 inference에서 자주 만나는 per-channel, per-token, group-wise를 중심으로 보면 된다.

판단 규칙

Granularity는 항상 세 가지를 같이 본다.

accuracy: scale이 더 세밀하면 outlier penalty가 줄어드는가?
metadata: scale을 얼마나 많이 저장해야 하는가?
kernel: runtime이 그 layout을 빠르게 실행할 수 있는가?

요약하면 다음과 같다.

coarser granularity
-> simpler metadata and kernels
-> higher risk of wasted integer range

finer granularity
-> better local fit
-> more scale metadata and kernel constraints

확인

  • Per-tensor quantization이 outlier 하나 때문에 precision을 낭비하는 이유는 무엇인가?
  • INT8 weight에서는 per-channel이 충분한 경우가 많은데, INT4 weight에서는 group quantization이 필요한 이유는 무엇인가?
  • Granularity를 고를 때 accuracy만 보면 안 되고 kernel layout까지 봐야 하는 이유는 무엇인가?