AI 다이어트 실패 사건 · 보정 자료가 범위를 정한다 · 실습
보정 자료가 정확도를 정한다 — 좁게·맞게·넓게 세 번
목표
CalibrationDataReader 를 직접 구현하고, 같은 모델을 좁은·맞는·넓은 보정 자료 세 벌로 정적 양자화해 그래프에 박히는 입력 스케일과 자기가 잰 최대 절대 오차를 나란히 남긴다. 그리고 어긋난 보정은 표본 수로 못 고친다는 것과, QDQ 와 QOperator 가 같은 계산의 다른 모양이라는 것을 재어서 확인한다.
왜 중요한가
정적 양자화는 활성의 스케일을 변환 시점에 못박고, 그 스케일은 보정 자료가 정한다. 그래서 보정 자료는 설정값이 아니라 입력값이다. 같은 모델과 같은 명령이라도 보정 자료가 다르면 완전히 다른 모델이 나온다. 그런데 그 자료는 코드에도 로그에도 남지 않는다.
무너지는 방향은 둘이다. 보정이 본 범위가 실제보다 좁으면 밖으로 나간 값이 정수 자료형의 끝에 붙어 버리고 역양자화로 돌아오지 않는다. 넓으면 잘리지는 않지만 쓰이지도 않는 범위에 눈금을 나눠 주므로 실제로 값이 사는 구간의 해상도가 떨어진다. 어느 쪽인지는 오차 하나만 봐서는 모르고, 세 벌로 재어 봐야 갈린다.
그리고 표본 수로 못 고치는 종류가 있다. 보정 자료의 분포 자체가 어긋나 있으면 그 자료를 512배로 늘려도 관찰 범위는 조금밖에 안 넓어진다. 수가 아니라 분포가 문제일 때를 구분할 수 있어야 시간을 엉뚱한 곳에 쓰지 않는다.
채점기는 여러분이 적어 낸 숫자를 믿지 않는다. 여러분의 공급기를 직접 몰아 계약을 지키는지 보고, 채점기가 지은 모델에 여러분의 도구를 걸어 보고, 적어 낸 오차를 여러분의 모델 파일로 다시 계산해 대조한다.
단계
1. /root/onnxq-calib/gen_model.py 를 만들어 실행해 /root/onnxq-calib/fp32.onnx 를 만드세요.
2. /root/onnxq-calib/reader.py 에 make_reader(arrays, input_name) 를 만드세요.
3. /root/onnxq-calib/calibtool.py 에 quantize 와 scales 를 만들어 /root/onnxq-calib/int8_match.onnx 와 /root/onnxq-calib/match.json 을 만드세요.
4. error 를 더하고 좁은 보정으로 /root/onnxq-calib/int8_narrow.onnx 와 /root/onnxq-calib/narrow.json 을 만드세요.
5. 넓은 보정으로 /root/onnxq-calib/int8_wide.onnx 와 /root/onnxq-calib/wide.json 을 만드세요.
6. 셋을 한자리에 모아 /root/onnxq-calib/calib_report.json 을 만드세요.
7. 좁은 보정을 1·8·64·512 묶음으로 늘려 보고 /root/onnxq-calib/samples.json 에 적으세요.
8. 같은 보정으로 QOperator 형식도 뽑아 /root/onnxq-calib/format.json 과 /root/onnxq-calib/report.md 를 만드세요.
참고
- 파이썬은
/opt/onnx-lab/bin/python입니다. 시스템python3에는 numpy 도 onnx 도 없습니다. 실행 예:/opt/onnx-lab/bin/python /root/onnxq-calib/calibtool.py scales /root/onnxq-calib/int8_match.onnx. - 실행 계약: 성공하면 종료 코드 0 이고 표준출력에 JSON 한 덩어리를 냅니다. 인자 수가 맞지 않으면 2 입니다.
quantize <입력.onnx> <출력.onnx> <씨앗> <배율> <묶음수> [형식]응답:{"out", "bytes", "batches", "span", "format", "ops", "input_scale", "input_zero_point"}. 형식은qdq(기본) 또는qoperator입니다. 활성과 가중치 모두 QInt8 입니다.scales <모델.onnx>응답:{"input_scale", "input_zero_point", "observed_span", "quantize_nodes", "dequantize_nodes"}. observed_span 은 input_scale 에 255 를 곱한 값입니다.error <fp32.onnx> <int8.onnx> <씨앗> <행>응답:{"rows", "max_abs_error", "mean_abs_error", "max_abs_output"}.- 보정 자료를 만드는 규칙(채점기가 같은 자료를 다시 만듭니다): 한 묶음은 16행입니다. 특징 수는 모델 입력의 마지막 축 크기입니다.
rng = numpy.random.default_rng(씨앗)으로 묶음마다(rng.normal(0, 1, (16, 특징)) * 배율).astype(numpy.float32)를 만들어 묶음 수만큼 냅니다. 곱한 뒤에 float32 로 내립니다. - 평가 자료를 만드는 규칙:
numpy.random.default_rng(씨앗).normal(0, 1, (행, 특징)).astype(numpy.float32)입니다. 배율을 곱하지 않습니다 — 이것이 실제 분포입니다. - 이 실습이 쓰는 값: 보정 씨앗 4242, 평가 씨앗 777, 평가 행 256, 묶음 수 64. 배율은 좁게 0.2 · 맞게 1.0 · 넓게 10.0 입니다.
make_reader(arrays, input_name)는onnxruntime.quantization.CalibrationDataReader를 상속한 객체를 돌려줘야 합니다.get_next()는{입력이름: 배열}사전을 하나씩 돌려주고 자료가 떨어지면None을 돌려줍니다. 끝난 뒤에 또 불려도 계속 None 이어야 합니다 — 예외를 던지면 변환기가 보정 도중에 죽습니다.- 소진한 공급기는 재사용할 수 없습니다. 양자화할 때마다 새로 만드세요.
- 모델에 박힌 입력 스케일은 그래프에서 입력 텐서를 첫 입력으로 받는
QuantizeLinear(QOperator 형식이면QLinearMatMul)를 찾아 그 스케일 초기자를 읽으면 됩니다. calibtool.py는 같은 디렉터리의reader.py를 가져다 씁니다. 스크립트가 있는 디렉터리를sys.path에 넣으세요.- 7단계가 만든 모델은 /root/onnxq-calib/samples 아래에
narrow_n1.onnxnarrow_n8.onnxnarrow_n64.onnxnarrow_n512.onnxmatch_n8.onnx로 저장합니다. 512묶음은 시간이 조금 걸립니다. quantize_static을 부르면 전처리를 권하는 경고가 납니다. 이 실습의 모델은 이미 단순해서 전처리 없이도 그대로 동작합니다.- 공식 문서: [ONNX Runtime 양자화](https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html) · [QuantizeLinear](https://onnx.ai/onnx/operators/onnx__QuantizeLinear.html) · [DequantizeLinear](https://onnx.ai/onnx/operators/onnx__DequantizeLinear.html) · [ONNX Concepts](https://onnx.ai/onnx/intro/concepts.html)
- 흔한 실수: 공급기를 한 번 만들어 여러 변환에 돌려쓰기, 끝난 뒤 예외 던지기, 평가 자료에 배율을 곱하기, 형식이 다르면 정확도도 다를 것이라 짐작하기.
단계 8개
- 잴 대상을 손에 쥔다
- 변환기에 자료를 흘려 넣는 계약
- 맞는 보정으로 한 번
- 좁은 보정 — 잘린다
- 넓은 보정 — 뭉개진다
- 셋을 한자리에 놓기
- 표본 수로는 못 고치는 것
- 같은 계산, 다른 모양