Activation Quantization and Outliers
마지막 수정:
Weight-only quantization은 weight traffic을 줄인다. Activation quantization은 한 단계 더 들어간다.
W8A8:
weights -> 8-bit
activations -> 8-bit
Activation까지 낮은 precision으로 내려가야 low-precision GEMM kernel을 더 직접적으로 쓸 수 있다. 그래서 activation quantization은 memory saving만이 아니라 compute throughput과 연결된다.
W4A16
low concurrency decode- Weights
- 4-bit
- Activations
- 16-bit
- Main bottleneck
- memory bandwidth
W8A8 / FP8
high batch / prefill- Weights
- 8-bit
- Activations
- 8-bit
- Main bottleneck
- compute throughput
LLM.int8()
fast safe deployment- Weights
- 8-bit + FP16 outliers
- Activations
- 8-bit + FP16 path
- Main bottleneck
- activation outliers
Activation은 weight보다 어렵다
Weight는 학습이 끝나면 고정된다.
same weight tensor
same distribution
offline calibration possible
Activation은 매 input마다 달라진다.
same layer
different prompt
different sequence
different activation range
그래서 activation quantization은 두 질문을 항상 포함한다.
scale을 배포 전에 고정할 것인가?
scale을 inference 중에 계산할 것인가?
Static scaling은 빠르지만 calibration mismatch에 약하다. Dynamic scaling은 현재 input에 맞게 range를 잡지만 runtime overhead가 생긴다.
W8A8은 compute-bound workload에서 강하다
W4A16은 weight를 4-bit로 줄이기 때문에 model footprint와 weight bandwidth 절감이 크다. Decode가 memory-bound일 때 특히 좋다.
하지만 activation은 여전히 FP16/BF16이고, compute 경로도 high precision에 남아 있을 수 있다.
반면 W8A8이나 FP8은 weight와 activation을 모두 낮춘다.
W4A16:
memory traffic relief
W8A8 / FP8:
memory traffic relief + lower-precision compute
그래서 high batch, long-context prefill, high-throughput serving처럼 compute-bound 쪽으로 밀리는 상황에서는 activation quantization이 더 의미 있어진다.
문제는 activation outlier다
LLM activation은 종종 heavy-tailed distribution을 가진다. 대부분 값은 작지만 일부 channel이나 token이 매우 큰 값을 만든다.
99% values: small
1% or less: huge
Per-tensor activation quantization은 이 outlier에 취약하다.
one huge channel sets the scale
-> step size becomes large
-> normal channels use only a few integer levels
-> information is crushed near zero
이건 단순 clipping 문제만이 아니다. Outlier를 보호하느라 scale이 커지고, 정상 값들의 resolution이 사라지는 scale-poisoning 문제다.
Per-token scale은 LLM activation의 기본 도구다
Transformer에서는 token마다 activation magnitude가 다를 수 있다.
token A: small range
token B: large range
Sequence 전체에 scale 하나를 쓰면 가장 큰 token이 scale을 결정한다. 그러면 다른 token들은 integer range를 낭비한다.
그래서 LLM activation quantization에서는 dynamic per-token scale이 중요하다.
[batch, seq_len, hidden]
-> each token gets its own scale over hidden dimension
이 방식은 scale metadata와 runtime 계산 비용이 있지만, transformer activation의 input-dependent range를 따라갈 수 있다.
LLM.int8()는 outlier dimension을 분리한다
LLM.int8()는 activation outlier를 한 grid 안에 억지로 넣지 않는다.
normal dimensions
-> INT8 path
outlier dimensions
-> FP16 path
outputs
-> sum
핵심은 outlier가 activation에서 발생한다는 점이다. Weight distribution은 비교적 잘 behaved할 수 있지만, activation의 일부 hidden dimension이 100배 이상 커질 수 있다.
이런 dimension을 INT8 grid에 같이 넣으면 전체 scale이 망가진다. 그래서 작은 수의 outlier dimension은 full precision으로 보내고, 나머지 대부분은 INT8로 계산한다.
책의 OPT-6.7B 예시에서는 이런 outlier dimension이 layer마다 매우 작은 비율이면서도 품질에 큰 영향을 준다. 이 때문에 0.1% 수준의 full-precision path가 전체 INT8 경로를 살릴 수 있다.
SmoothQuant 계열의 위치
SmoothQuant 계열은 activation outlier를 직접 낮추기 위해 equivalent transformation을 사용한다.
아이디어는 이렇다.
W x = (W s) (x / s)
즉 activation 쪽의 큰 range를 줄이고, 그 부담을 weight 쪽 scale로 옮긴다. Weight는 offline으로 다루기 쉬우므로 activation quantization이 쉬워진다.
이 path에서는 SmoothQuant를 깊게 파기 전에 먼저 원리를 기억하면 된다.
activation outlier를 그대로 clip하지 말고
quantize하기 쉬운 쪽으로 range 부담을 이동한다
AWQ도 비슷한 equivalent transformation 감각을 공유하지만 목표가 다르다. AWQ는 weight-only INT4에서 중요한 weight channel을 보호하는 쪽이고, SmoothQuant는 W8A8처럼 activation quantization을 가능하게 하는 쪽에 가깝다.
FP8은 hardware와 같이 봐야 한다
FP8은 INT8보다 floating-point range 구조를 가져서 LLM activation에 유리할 수 있다. 하지만 format이 좋아 보여도 hardware가 해당 kernel을 빠르게 실행하지 못하면 기대한 이득이 나오지 않는다.
따라서 activation quantization은 algorithm만의 결정이 아니다.
format: INT8? FP8?
scale: static? dynamic? per-token?
kernel: GPU/runtime support?
workload: prefill? decode? high batch?
이 네 가지가 같이 맞아야 실제 serving 성능이 나온다.
확인
- Weight-only quantization과 activation quantization의 성능 이득이 다른 이유는 무엇인가?
- Per-tensor activation quantization이 LLM outlier에 약한 이유는 무엇인가?
- LLM.int8()가 outlier dimension을 FP16 path로 분리하는 이유는 무엇인가?