AI 다이어트 실패 사건 · 보정 자료가 범위를 정한다 · 이론
보정 자료가 자를 만든다 — 좁으면 잘리고 넓으면 뭉개진다
한 줄 요약
정적 양자화에서 스케일은 보정 자료가 정한다. 보정이 본 범위가 실제 분포보다 좁으면 값이 잘리고, 넓으면 눈금이 성글어진다 — 어느 쪽이든 정확도는 보정 자료의 함수다.
왜 여기서 자꾸 무너지나
정적 양자화는 활성의 스케일을 변환 시점에 못박는다. 못박으려면 활성이 어느 범위에 사는지 알아야 하고, 그것을 알려 주는 것이 보정 자료다. 변환기는 이 자료를 흘려 넣어 층마다 최솟값과 최댓값을 관찰하고, 그 범위로 스케일과 영점을 만든다.
그래서 보정 자료는 설정값이 아니라 입력값이다. 같은 모델, 같은 변환 명령인데 보정 자료만 바꾸면 완전히 다른 모델이 나온다. 그런데 이 자료는 코드에 남지 않고 로그에도 안 남는다. 몇 달 뒤 "이 모델 정확도가 왜 이렇지" 를 보면 모델 파일과 변환 스크립트만 있고, 정작 답을 정한 것은 그날 쓴 배열들이다.
무너지는 방향은 둘이다. 좁으면 잘린다. 보정 때 본 최댓값보다 큰 값이 들어오면 정수 자료형의 끝에 붙어 버리고 역양자화로 돌아오지 않는다. 오차가 왕복 상한의 수십 배로 커진다. 넓으면 뭉개진다. 실제로는 쓰이지 않는 넓은 범위에 눈금 256개를 배분하므로, 자료가 실제로 사는 구간에는 몇 개 안 되는 눈금만 놓인다. 잘리지는 않지만 해상도를 잃는다.
어떻게 동작하나
[ONNX Runtime 의 양자화 문서](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html)가 정적 양자화에 CalibrationDataReader 를 넘기라고 한다. 계약은 짧다.
class 내_공급기(CalibrationDataReader): def get_next(self): 더 줄 것이 있으면 -> {"입력이름": 배열} 사전 하나 더 없으면 -> None변환기는 None 이 나올 때까지 get_next() 를 계속 부른다. 그래서 두 가지 실수가 흔하다. 하나는 끝난 뒤에 None 대신 예외를 던지는 것이고(보정 도중에 죽는다), 다른 하나는 한 번 소진한 공급기를 다시 쓰는 것이다(두 번째 변환은 보정 자료를 하나도 못 받는다).
보정이 남긴 흔적은 모델 안에 그대로 있다. 그래프를 열어 입력 바로 뒤의 QuantizeLinear 를 찾아 그 스케일 초기자를 읽으면, 스케일에 눈금 수를 곱한 값이 보정 때 관찰한 범위의 폭이다. 이 숫자 하나로 "보정 자료가 어디까지 봤는가" 를 사후에 확인할 수 있다.
형식도 두 가지다. QDQ 는 원래 연산자를 그대로 두고 앞뒤에 QuantizeLinear 와 DequantizeLinear 를 끼워 넣는다. 그래프에 MatMul 이 그대로 보이고, 정수화는 런타임이 실행 시에 합친다. QOperator 는 QLinearMatMul 같은 정수 연산자로 아예 갈아 끼운다. 노드 수가 적고 파일도 작지만 그래프를 읽기는 어렵다.
여기서 흔한 오해 하나를 짚어야 한다. 같은 보정에서 뽑은 두 형식은 같은 스케일과 영점을 쓰고 오차의 크기도 같은 자릿수지만, 원소별로 똑같지는 않다. QDQ 는 원래 연산자를 그래프에 남겨 두고 실행 방식을 런타임에 맡기기 때문이다. 런타임이 Q/DQ 를 접어 정수 커널로 돌릴 수도 있고, 역양자화한 값으로 그냥 실수 연산을 할 수도 있다. 그래서 "형식을 바꿨더니 출력이 조금 달라졌다" 는 정상이고, 자릿수가 달라졌다면 그때는 보정을 의심해야 한다.
현장에서 만나는 모습
첫째, 표본을 늘려도 안 고쳐지는 종류가 있다. 보정 자료의 분포 자체가 어긋나 있으면, 그 자료를 몇 백 배로 늘려도 관찰 범위는 조금밖에 안 넓어진다. 좁은 보정을 512배로 늘려 얻는 개선이, 맞는 분포로 몇 묶음만 준 것보다 못하다. 수가 아니라 분포가 문제일 때가 있다는 것을 재어서 구분해야 한다.
둘째, 정적이 동적보다 항상 정확하지는 않다. "정적이 더 좋다" 는 말은 보정이 제대로 됐을 때의 이야기다. 보정이 어긋나면 동적 양자화보다 훨씬 나쁜 모델이 나온다. 그리고 그 사실은 평가해 보기 전까지 아무 데도 안 나타난다.
셋째, 잘림과 뭉개짐은 증상이 다르다. 잘림은 큰 입력에서만 크게 틀리고 작은 입력에서는 멀쩡하다. 뭉개짐은 모든 입력에서 고르게 조금씩 틀린다. 오차를 입력 크기별로 나눠 보면 어느 쪽인지 갈린다.
넷째, 보정 자료를 기록으로 남긴다. 어떤 표본을 몇 개 썼는지, 그 자료의 범위가 얼마였는지를 모델 옆에 적어 둔다. 모델 안의 스케일과 대조할 수 있어야 나중에 원인을 찾는다.
실무에서 진짜 중요한 것
- 보정 자료는 배포 때 들어올 자료와 같은 분포에서 뽑는다. 이것이 표본 수보다 먼저다.
- 모델에 박힌 스케일을 꺼내 확인한다. 스케일 곱하기 눈금 수가 보정이 본 범위다.
- 세 벌로 재어 본다. 좁게·맞게·넓게 세 번 양자화해 보면 자기 자료의 민감도가 보인다.
- 형식은 표현의 차이이고 실행은 런타임이 정한다. 형식을 바꿔 오차가 자릿수만큼 달라졌다면 보정을 의심한다.
다음 실습에서 할 것
CalibrationDataReader 를 직접 구현하고, 같은 모델을 좁은·맞는·넓은 보정 자료 세 벌로 양자화해 그래프에 박힌 입력 스케일과 자기가 잰 최대 절대 오차를 나란히 적는다. 그다음 좁은 보정을 1묶음에서 512묶음까지 늘려 가며 그것으로는 못 고친다는 것을 재고, 마지막으로 QDQ 와 QOperator 를 같은 보정에서 뽑아, 두 형식의 오차 크기와 둘 사이의 차이를 함께 잰다. 채점기는 여러분의 공급기를 직접 몰아 보고, 적어 낸 오차를 여러분의 모델 파일로 다시 계산해 대조한다.