Quantization Granularity
마지막 수정:
Quantization에서 bit-width만큼 중요한 질문이 있다.
scale 하나를 몇 개의 값이 공유할 것인가?
이 질문을 granularity라고 부른다.
Per-tensor
1 scale / tensor
simple baselinePer-channel
1 scale / output channel
INT8 weightsPer-token
1 scale / token activation
LLM activationsGroup-128
1 scale / 128 weights
INT4 weightsBlock
1 scale / 2D tile
specialized kernelsCoarse 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까지 봐야 하는 이유는 무엇인가?