Activation Quantization and Outliers

마지막 수정:

quantizationactivationsw8a8fp8llm-int8outliersinference

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 quantization is what lets kernels use lower-precision compute. It is harder than weight-only quantization because activation ranges change with every input.

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로 분리하는 이유는 무엇인가?