AI 다이어트 실패 사건 · 동적 양자화 · 실습
보정 자료 없이 — 동적 양자화를 재어 본다
목표
dyntool.py 를 만들어 자기 모델에 동적 양자화를 걸고, 그래프가 어떻게 바뀌는지, 파일이 어디서 줄고 어디가 그대로인지, 입력 분포가 달라져도 왜 버티는지, 그리고 이 방식이 맞지 않는 경우가 어떤 모습인지를 전부 숫자로 남긴다.
왜 중요한가
정적 양자화는 대표 입력을 모아야 시작할 수 있고, 그 자료를 구하는 일이 현장에서 가장 오래 걸린다. 동적 양자화는 활성의 범위를 미리 정하지 않고 추론할 때마다 그 텐서를 보고 정하므로 모델 파일 하나만 있으면 된다. 그래서 무엇을 할지 정하기 전에 먼저 걸어 보는 기준선이 된다.
대신 무엇을 얻고 무엇을 잃는지 알아야 고를 수 있다. 얻는 것은 분포가 흔들려도 버티는 성질이다 — 배치마다 자를 다시 만드니 보정 때 본 범위 밖으로 나갈 일이 없다. 잃는 것은 재현성이다. 스케일이 그 배치 전체의 최댓값으로 정해지므로 이상값 한 줄이 같은 배치의 다른 줄까지 끌어내린다. 혼자 넣으면 멀쩡하던 입력이 이웃 때문에 나빠지는 것이다.
파일 크기도 재어 봐야 안다. 줄어드는 것은 행렬뿐이고 편향은 float32 로 남으며, 줄어든 행렬 옆에는 스케일과 영점이 따라붙는다. 가중치가 작은 모델에서는 새로 들어온 노드가 줄어든 양보다 커서 파일이 오히려 커진다.
채점기는 여러분이 적어 낸 숫자를 믿지 않는다. 매번 다른 씨앗과 모양으로 모델을 새로 지어 여러분의 도구를 실제로 실행하고, DynamicQuantizeLinear 연산자의 실제 결과 그리고 채점기가 직접 돌린 추론 결과와 대조한다.
단계
1. /root/onnxq-dyn/gen_model.py 를 만들어 실행해 /root/onnxq-dyn/fp32.onnx 를 만드세요.
2. /root/onnxq-dyn/dyntool.py 에 quantize 를 만들어 /root/onnxq-dyn/dyn.onnx 를 만드세요.
3. sizes 를 더해 초기자별 바이트를 세고 /root/onnxq-dyn/size.json 에 적으세요.
4. dqparams 를 더해 DynamicQuantizeLinear 가 추론마다 하는 계산을 직접 구현하세요.
5. compare 를 더해 입력 배율을 바꿔 가며 오차를 재고 /root/onnxq-dyn/batches.json 에 적으세요.
6. outlier 를 더해 이상값 한 줄이 배치를 흔드는지 재고 /root/onnxq-dyn/outlier.json 에 적으세요.
7. /root/onnxq-dyn/gen_small.py 로 아주 작은 모델을 만들어 양자화하고 /root/onnxq-dyn/unfit.json 에 적으세요.
8. /root/onnxq-dyn/report.json 과 /root/onnxq-dyn/report.md 로 한 장에 정리하세요.
참고
- 파이썬은
/opt/onnx-lab/bin/python입니다. 시스템python3에는 numpy 도 onnx 도 없습니다. 실행 예:/opt/onnx-lab/bin/python /root/onnxq-dyn/dyntool.py sizes /root/onnxq-dyn/fp32.onnx. - 실행 계약: 성공하면 종료 코드 0 이고 표준출력에 JSON 한 덩어리를 냅니다. 인자 수가 맞지 않으면 2 입니다.
quantize <입력.onnx> <출력.onnx>응답:{"out": 경로, "bytes": 정수, "ops": {연산자: 개수}, "removed": [사라진 연산자], "added": [새로 든 연산자]}. 가중치는 QInt8 입니다.sizes <모델.onnx>응답:{"file_bytes": 정수, "initializers": {이름: {"dtype": 문자열, "elements": 정수, "bytes": 정수}}, "initializer_bytes": 정수}.dqparams <입력.npy> <출력.npy>응답:{"scale": 실수, "zero_point": 정수, "min": 실수, "max": 실수, "count": 정수}. 양자화한 uint8 배열을 출력 경로에 저장합니다.compare <fp32.onnx> <int8.onnx> <씨앗> <행> <배율>응답:{"rows", "span", "max_abs_output", "max_abs_error", "relative"}. relative 는 오차를 fp32 출력의 최대 절댓값으로 나눈 값입니다.outlier <fp32.onnx> <int8.onnx> <씨앗> <행> <튀는값>응답:{"rows", "spike", "clean", "with_spike", "ratio"}.- 배치를 만드는 규칙(채점기가 같은 배치를 다시 만듭니다): 특징 수는 모델 입력의 마지막 축 크기입니다.
rng = numpy.random.default_rng(씨앗)으로(rng.normal(0, 1, (행, 특징)) * 배율).astype(numpy.float32)입니다. 곱한 뒤에 float32 로 내립니다. outlier의 규칙: 위 규칙을 배율 1.0 으로 써서 배치를 만들고, clean 은 그 배치만 넣었을 때의 최대 절대 오차입니다. with_spike 는numpy.full((1, 특징), 튀는값, dtype=numpy.float32)한 줄을 아래에 붙여 넣은 뒤 앞의 행 개만큼에서 잰 최대 절대 오차입니다. ratio 는 with_spike 를 clean 으로 나눈 값입니다.- DynamicQuantizeLinear 의 정의: 범위는
[min(x, 0), max(x, 0)]이고 스케일은 그 폭을 255 로 나눈 값이며, 영점은 실수 0.0 이 앉을 정수 자리를 0 부터 255 사이로 자른 것입니다. 반올림은 numpy 의np.rint와 같은 짝수 쪽 붙이기입니다. - 스케일은
np.float32(...)로 만들고 나눗셈도 float32 로 하세요. 파이썬 float 으로 나누면 경계에 걸린 원소에서 런타임과 한 눈금 어긋납니다. fp32.onnx의 배치 축은 이름만 있는 동적 축이어야 합니다(["N", 20]). 숫자로 굳히면 채점기가 다른 행 수를 넣지 못합니다.- 5단계는 배율 0.05 · 1.0 · 50.0 을 씨앗 20260917 · 행 32 로 잽니다. 6단계는 씨앗 20260917 · 행 12 · 튀는값 60.0 입니다.
- 공식 문서: [ONNX Runtime 양자화](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html) · [DynamicQuantizeLinear](https://onnx.ai/onnx/operators/onnx__DynamicQuantizeLinear.html) · [MatMulInteger](https://onnx.ai/onnx/operators/onnx__MatMulInteger.html) · [ONNX Concepts](https://onnx.ai/onnx/intro/concepts.html)
quantize_dynamic을 부르면 전처리를 권하는 경고가 납니다. 이 실습의 모델은 이미 단순해서 전처리 없이도 그대로 동작합니다.- 흔한 실수: 파일이 줄었을 것이라 짐작하고 재지 않기, 편향까지 int8 이 되었다고 적기, 배치 규칙에서 float32 로 내리는 순서 바꾸기, 이상값 실험에서 튀는 줄까지 포함해 오차를 재기.
단계 8개
- 잴 대상을 손에 쥔다
- 명령 한 줄로 가중치만 굳히기
- 어디가 줄었는지 이름으로 말하기
- 추론마다 자를 다시 만드는 계산
- 입력을 1000배 흔들어 보기
- 이웃 한 줄이 배치를 흔든다
- 작은 모델에서는 오히려 커진다
- 얻은 것과 잃은 것을 한 장으로