LabHub
배우기 러닝패스 코스

The AI Diet Gone Wrong

Build the Ruler Yourself: Scale, Zero Point, Round-Trip Error

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

qmath.py 를 만들어 QuantizeLinear·DequantizeLinear 의 정의대로 스케일과 영점을 계산하고, 양자화와 역양자화를 구현하고, 왕복 오차가 스케일의 절반 안에 있는지 확인하고, 대칭과 비대칭 그리고 채널별 스케일과 텐서 전체 스케일을 같은 자료 위에서 견준다.

왜 중요한가

변환 도구가 내놓은 int8 모델의 정확도가 떨어졌을 때 고칠 곳을 고르려면, 그 도구가 안에서 하는 산술을 알아야 한다. 스케일은 관찰 범위를 눈금 수로 나눈 값이고, 영점은 실수 0.0 이 앉을 정수 자리다. 범위에 0 이 없으면 패딩과 ReLU 가 만든 0 이 0 이 아닌 값으로 되살아난다. 왕복 오차에는 상한이 있다. 값이 범위 안에 있는 한 오차는 스케일의 절반을 넘지 않는다 — 양자화가 값을 스케일 간격의 격자로 옮기는 일이기 때문이다. 그래서 오차가 그 절반을 크게 넘었다면 반올림이 아니라 범위가 문제다. 이 한 가지 잣대가 원인 판단을 빠르게 만든다. 자료의 모양도 방식을 정한다. ReLU 뒤의 활성은 전부 0 이상이라 대칭 int8 을 걸면 음수 쪽 눈금이 통째로 놀고 스케일이 두 배 거칠어진다. 가중치 행렬에 이상값이 하나 있으면 텐서 전체 스케일이 그 값에 끌려가 나머지가 전부 뭉개진다 — 채널을 나누면 피해가 그 채널 안에 갇힌다. 채점기는 여러분이 적어 낸 숫자를 믿지 않는다. 매번 다른 씨앗으로 배열을 만들어 여러분의 qmath.py 를 실제로 실행하고, 같은 입력을 ONNX 의 QuantizeLinear·DequantizeLinear 연산자에 그대로 태워 나온 결과와 대조한다.

단계

  1. /root/onnxq-scale/gen_data.py 를 만들어 실행해 /root/onnxq-scale/data 아래 배열 세 벌을 만들고, /root/onnxq-scale/qmath.pyparams 를 구현하세요.
  2. q 를 더해 실수 배열을 정수 배열로 내리세요.
  3. dq 를 더해 정수 배열을 실수로 되돌리세요.
  4. roundtrip 을 더해 왕복 오차와 그 상한을 함께 내게 하세요.
  5. 범위를 일부러 좁혀 포화를 만들고 결과를 /root/onnxq-scale/saturate.json 에 적으세요.
  6. 전부 0 이상인 자료에 대칭과 비대칭을 걸어 /root/onnxq-scale/symmetry.json 에 적으세요.
  7. channel 을 더해 채널별 스케일과 텐서 전체 스케일을 견주고 /root/onnxq-scale/channel.json 에 적으세요.
  8. /root/onnxq-scale/report.json/root/onnxq-scale/report.md 로 한 장에 정리하세요.

참고

관찰 범위를 스케일과 영점으로

/root/onnxq-scale/gen_data.py 를 만들어 실행해 /root/onnxq-scale/data 아래 세 배열을 만들고, /root/onnxq-scale/qmath.pyparams <lo> <hi> <mode> 를 구현하세요. 범위에 0 이 없으면 넣어야 합니다.

비대칭은 (hi - lo) 를 255 로 나누고, 영점은 실수 0.0 이 앉을 정수 자리입니다 — qmin 에서 lo/scale 을 뺀 값을 반올림하세요. 대칭은 좌우 폭 중 큰 쪽을 127 로 나누고 영점은 0 입니다. lo 가 0 보다 크거나 hi 가 0 보다 작은 경우를 먼저 처리하세요.

실수를 정수 눈금으로 내리기

q <입력.npy> <출력.npy> <mode> [lo hi] 를 더해 실수 배열을 정수 배열로 내리고 결과를 .npy 로 저장하게 하세요. 응답에는 scale·zero_point·clipped·dtype·count 가 있어야 합니다.

정의는 saturate(round(x / scale) + zero_point) 입니다. 나누고, 반올림하고, 영점을 더하고, 자료형의 양끝으로 자릅니다. 순서를 바꾸면 답이 달라집니다 — 영점을 먼저 더하고 반올림하면 안 됩니다. 자른 원소 수를 세어 clipped 로 내세요.

정수를 실수로 되돌리기

dq <양자.npy> <출력.npy> <scale> <zero_point> <mode> 를 더해 정수 배열을 float32 실수 배열로 되돌리게 하세요. 응답에는 min·max·count 가 있어야 합니다.

정의는 (q - zero_point) * scale 입니다. 이 방향에는 반올림도 포화도 없습니다 — 앞에서 잘려 나간 값은 여기서 되살아나지 않습니다. 정수 배열을 float32 로 먼저 올린 뒤 빼야 uint8 에서 음수가 감싸 도는 일을 피합니다.

왕복 오차와 그 상한

roundtrip <입력.npy> <mode> [lo hi] 를 더해 양자화하고 곧바로 역양자화한 뒤 최대 절대 오차와 상한을 함께 내게 하세요. bound 는 스케일의 절반이고 within_bound 는 오차가 그 안에 있는지입니다.

양자화는 값을 스케일 간격의 격자 위 가장 가까운 점으로 옮기는 일입니다. 격자 간격이 스케일이니 가장 멀어도 절반입니다. 부동소수점 때문에 아주 조금 넘칠 수 있으므로 비교에 약간의 여유를 두세요. 이 상한은 값이 범위 안에 있을 때만 성립합니다.

범위를 좁히면 무엇이 깨지는가

data/spread.npy 를 관찰 범위 그대로 한 번, 그리고 -1.0 1.0 으로 좁혀 한 번 왕복시키고 두 결과를 /root/onnxq-scale/saturate.json 에 full·narrow·error_ratio 로 적으세요.

좁은 범위는 스케일을 곱게 만듭니다 — 그 자체는 좋은 일입니다. 문제는 범위 밖으로 나간 값이 벽에 붙는다는 것입니다. 오차가 상한을 크게 넘으면 반올림이 아니라 범위가 원인입니다. error_ratio 는 좁힌 쪽 오차를 원래 오차로 나눈 값입니다.

0 이상만 나오는 자료에 대칭을 걸면

data/positive.npyasym-u8sym-i8 을 각각 걸어 /root/onnxq-scale/symmetry.json 에 asym·sym·scale_ratio 를 적으세요. 각 항목에는 scale·zero_point·max_abs_error·usable_levels 가 있어야 합니다.

usable_levels 는 범위 [lo, hi] 안에 들어가는 눈금의 개수입니다 — 범위 폭을 스케일로 나눈 뒤 1 을 더해 반올림하면 나옵니다. 전부 0 이상인 자료에서 비대칭은 256, 대칭은 128 이 나올 것입니다. 스케일 비가 그대로 오차 비가 되는지 확인해 보세요.

이상값 하나를 가두기

channel <입력.npy> <axis> 를 더해 대칭 int8 로 채널별 스케일과 텐서 전체 스케일을 견주고, data/weights.npy 를 axis 0 으로 돌린 결과를 /root/onnxq-scale/channel.json 에 적으세요.

축 하나를 채널로 보고 채널마다 min·max 를 따로 구해 스케일을 만듭니다. 채널별 스케일은 1차원 배열이고, 브로드캐스트를 위해 해당 축만 길이를 갖도록 모양을 바꿔야 합니다. channels_improved 는 채널별 쪽 오차가 텐서 전체 쪽보다 작아진 채널의 수입니다. 이상값이 든 채널은 나아지지 않는다는 점이 핵심입니다.

잰 것을 한 장으로

/root/onnxq-scale/report.json 에 bound_rule·spread·positive_asym_over_sym·channel_gain·saturation_ratio 를 적고, /root/onnxq-scale/report.md## 스케일과 영점은 어디서 나오나 ## 왕복 오차의 상한 ## 대칭과 비대칭 ## 이상값 하나가 하는 일 네 절로 쓰세요.

channel_gain 은 텐서 전체 오차를 채널별 오차로 나눈 값이 아니라 가장 좋아진 채널 기준이 더 정직합니다. 이상값이 든 채널은 그대로이므로 전체 최대 오차는 거의 같습니다. 보고서에는 숫자와 함께 그 숫자가 무엇을 가리키는지 한 줄씩 적으세요.