AI 다이어트 실패 사건 · 동적 양자화 · 이론
보정 자료 없이 — 동적 양자화가 그래프에 하는 일
한 줄 요약
동적 양자화는 가중치만 미리 int8 로 굳히고 활성의 범위는 추론할 때마다 그 자리에서 잰다. 그래서 보정 자료가 없어도 되고, 그 대가로 배치마다 다른 자를 쓴다.
왜 이 갈래가 필요했나
정적 양자화를 하려면 대표 입력이 있어야 한다. 활성이 대체로 어느 범위에 사는지 미리 보고 스케일을 못박아야 하기 때문이다. 그런데 현장에서 그 자료를 구하는 일이 가장 오래 걸린다. 고객 자료는 못 받고, 합성 자료는 분포가 다르고, 받은 자료는 평가용과 섞여 있다.
동적 양자화는 그 문제를 통째로 비껴간다. 활성의 범위를 미리 정하지 않고 실행 시에 그 텐서를 보고 정한다. 변환에 필요한 것은 모델 파일 하나뿐이고, 명령 한 줄이면 끝난다. 그래서 "일단 이걸로 얼마나 되나 보자" 가 가능하고, 정적 양자화를 할지 말지를 정하는 기준선이 된다.
어떻게 동작하나
[ONNX Runtime 의 양자화 문서](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)가 두 갈래를 나란히 설명한다. 동적 쪽은 가중치만 오프라인에서 정수로 바꾸고, 활성의 스케일과 영점은 실행 중에 계산한다.
그래프를 열어 보면 무슨 일이 일어났는지 바로 보인다. MatMul 하나가 이렇게 바뀐다.
바뀌기 전 X(float32) ──▶ MatMul(W float32) ──▶ h(float32)바뀐 뒤 X(float32) ──▶ DynamicQuantizeLinear ──▶ (q uint8, scale, zero_point) │ ▼ MatMulInteger(W int8) ──▶ (int32) │ Cast ──▶ Mul(scale 들) ──▶ h(float32)MatMul 은 사라지고 그 자리에 DynamicQuantizeLinear · MatMulInteger · Cast · Mul 이 들어온다. 앞의 노드가 활성을 그 자리에서 uint8 로 내리면서 스케일과 영점을 함께 내놓고, 정수 커널이 곱한 뒤, 두 스케일을 곱해 실수로 돌아온다.
[DynamicQuantizeLinear](https://onnx.ai/onnx/operators/onnx__DynamicQuantizeLinear.html)의 정의는 짧다. 자료의 최솟값과 최댓값에 0 을 보태 범위를 잡고, 그 폭을 255 로 나눠 스케일을 만들고, 0.0 이 앉을 정수 자리를 영점으로 삼는다. 이 계산이 추론마다 다시 일어난다는 것이 이 방식의 전부다.
파일 크기가 줄어드는 자리도 정해져 있다. 초기자를 하나씩 세어 보면 행렬만 줄고 편향은 그대로 float32 로 남는다. 그리고 줄어든 행렬 옆에는 스케일과 영점이 작은 초기자로 따라붙는다. "양자화하면 4분의 1" 이라는 통념은 행렬이 파일의 대부분일 때만 맞는 말이다.
현장에서 만나는 모습
첫째, 입력 크기가 달라져도 버틴다. 입력을 1000배 크게 넣어도 상대 오차가 거의 그대로다. 매 배치마다 자를 다시 만들기 때문이다. 정적 양자화는 같은 상황에서 보정 때 본 범위 밖으로 나가 통째로 잘린다. 보정 자료가 실제 분포와 다를 위험이 없다는 것이 이 방식의 가장 큰 장점이다.
둘째, 같은 배치에 섞인 이웃이 서로를 망친다. 스케일이 그 텐서 전체의 최댓값으로 정해지므로, 한 줄만 유난히 큰 값이 들어오면 나머지 줄이 쓸 수 있는 눈금이 줄어든다. 혼자 넣었을 때는 멀쩡하던 입력이 배치에 끼면 답이 나빠진다 — 같은 입력인데 결과가 배치 구성에 따라 달라진다는 뜻이고, 재현이 필요한 자리에서는 이것이 곧바로 문제가 된다.
셋째, 작은 모델에서는 파일이 오히려 커진다. 가중치가 몇 백 바이트뿐인 모델에 동적 양자화를 걸면 새로 들어온 노드와 스케일·영점 초기자가 줄어든 양보다 크다. 재어 보지 않고 "작아졌겠지" 하고 넘어가면 그대로 배포된다.
넷째, 범위를 재는 일이 추론마다 일어난다. 스케일을 구하려면 활성 텐서를 처음부터 끝까지 훑어 최솟값과 최댓값을 찾아야 한다. 그래프에 DynamicQuantizeLinear 가 몇 개인지가 곧 그 훑기가 추론마다 몇 번 도는지다. 시간을 재기 전에 노드 수를 먼저 세어 보면 어디에 값이 붙는지 알 수 있다.
실무에서 진짜 중요한 것
- 먼저 동적으로 해 본다. 자료가 없어도 되고 한 줄이면 되니 기준선으로 삼기 좋다.
- 바뀐 그래프를 열어 본다. 어떤 노드가 사라지고 무엇이 들어왔는지가 전부를 설명한다.
- 파일 크기를 초기자 단위로 센다. 무엇이 줄었고 무엇이 그대로인지 이름으로 말할 수 있어야 한다.
- 배치 구성이 답을 바꾸는지 확인한다. 이상값 한 줄로 시험해 보면 바로 드러난다.
다음 실습에서 할 것
dyntool.py 를 만들어 자기 모델을 동적 양자화하고, 그래프 변화와 초기자별 바이트를 세고, DynamicQuantizeLinear 가 추론마다 하는 계산을 손으로 구현하고, 입력 배율을 1000배로 흔들어 가며 오차를 재고, 이상값 한 줄이 배치를 어떻게 흔드는지 재고, 아주 작은 모델에서 파일이 커지는 것을 확인한다. 채점기는 매번 다른 씨앗과 모양으로 모델과 배치를 새로 차려 여러분의 도구를 실제로 돌리고 같은 계산을 다시 해 대조한다.