트랜스포머 — 어텐션을 손으로 계산한다 · 정수 양자화가 정확도에 주는 영향 · 실습
int8 의 산술을 손으로 한다
목표
정수 양자화의 산술을 표준 라이브러리만으로 직접 한다. 반올림 규칙을 못 박는 것에서 시작해 대칭·비대칭 양자화를 만들고, 접었다 편 값의 오차를 재고, 이상치 하나가 배율을 얼마나 망가뜨리는지 세고, 텐서 단위와 행 단위를 나란히 견주고, 코드끼리 정수로만 곱하고, 마지막으로 그 오차가 소프트맥스를 지나 확률에서 얼마가 되는지 잰다.
왜 중요한가
양자화 도구는 한 줄이다. 그 안에서 무슨 산술이 일어났는지 모르면 정확도가 떨어졌을 때 옵션을 바꿔 가며 다시 돌려 보는 것밖에 할 수 없다. 배율이 무엇으로 정해지는지, 반올림이 어느 쪽으로 가는지, 단위를 좁히면 무엇이 달라지는지를 값 여덟 개짜리 목록에서 한 번 보고 나면 큰 모델에서도 같은 자리를 짚을 수 있다.
이 실습은 도구를 쓰지 않는다. 이 파드의 시스템 파이썬에는 numpy 가 없고(numpy 는 /opt/onnx-lab/bin/python 안에만 있습니다) 모델도 부르지 않는다. 그래서 "어느 모델이 int8 에서 정확도가 몇 퍼센트 떨어진다" 같은 말은 여기서 하지 않는다. 여러분이 만든 목록과 행렬에서 잰 숫자만 쓴다.
어려운 것은 식이 아니라 세부다. 0.5 를 어느 쪽으로 보낼지, 범위 밖을 자를지 말지, 배율을 무엇의 최대값으로 잡을지가 정해져 있지 않으면 같은 입력으로도 코드가 한 칸씩 어긋난다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 입력으로 함수를 두드려 보고, 채점기가 따로 계산한 값과 대조한다. 대조의 대부분은 정수 배열끼리라 흔들림이 없다.
단계
1. /root/work/tf-quant/quant.py 에 QMAX = 127·UMAX = 255 와 round_half_even(x)·round_half_away(x)·rounding_gap(values) 를 만드세요. 두 반올림 규칙이 어디서 갈리는지 눈으로 확인합니다.
2. sym_scale(values)·quantize_sym(values, scale)·dequantize_sym(codes, scale) 를 더해 대칭 양자화를 만드세요. 배율은 max(|x|) / 127 입니다.
3. affine_params(values)·quantize_affine(values, scale, zero_point)·dequantize_affine(codes, scale, zero_point) 를 더해 비대칭 양자화를 만드세요. 배율은 (max - min) / 255 입니다.
4. levels_used(codes) 와 error_stats(original, restored) 를 더해 오차를 재는 자를 만드세요.
5. outlier_effect(values, outlier) 를 더해 큰 값 하나가 나머지 값들에게 무슨 일을 하는지 재게 하세요.
6. quantize_tensor(matrix)·quantize_rows(matrix)·granularity_gap(matrix) 를 더해 텐서 단위와 행 단위를 견주세요.
7. transpose(matrix)·int_matmul(left_codes, right_codes)·float_matmul(left, right)·quant_matmul(left, right) 를 더해 정수만으로 행렬을 곱하세요.
8. WEIGHTS·OUTLIER·QUERIES·KEYS 와 softmax(scores)·attention_shift(queries, keys) 를 만들고, /root/work/tf-quant/quant_report.json 과 /root/work/tf-quant/quant_report.md 에 결과를 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/tf-quant/quant.py를 파이썬 모듈로 불러QMAX·UMAX·round_half_even·round_half_away·rounding_gap·sym_scale·quantize_sym·dequantize_sym·affine_params·quantize_affine·dequantize_affine·levels_used·error_stats·outlier_effect·quantize_tensor·quantize_rows·granularity_gap·transpose·int_matmul·float_matmul·quant_matmul·softmax·attention_shift·WEIGHTS·OUTLIER·QUERIES·KEYS를 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. round_half_even(x)는 파이썬 기본round그대로입니다. 정확히 0.5 인 자리가 짝수 쪽으로 갑니다 —round(0.5)는 0,round(1.5)는 2,round(2.5)는 2 입니다. 직접 돌려 확인하세요. 돌려주는 값은 정수입니다.round_half_away(x)는 정확히 0.5 인 자리를 0 에서 먼 쪽으로 보냅니다.math.floor(x + 0.5)한 줄로 쓰면 음수에서 틀립니다 —-1.5는-2로 가야 합니다.rounding_gap(values)는 두 규칙이 갈리는 값만 받은 순서 그대로 모아 돌려줍니다. 0.5 로 끝난다고 늘 갈리지는 않습니다.sym_scale(values)는max(|x|) / QMAX입니다. 목록의 값이 전부 0 이면 나눌 수 없으므로1.0을 돌려주세요.quantize_sym(values, scale)은 배율로 나누고round_half_even으로 반올림한 뒤-QMAX부터QMAX까지로 잘라 냅니다.dequantize_sym(codes, scale)은 배율을 곱할 뿐입니다.affine_params(values)는(scale, zero_point)를 돌려줍니다.scale = (max - min) / UMAX,zero_point = round_half_even(-min / scale)을 0 부터UMAX까지로 자른 값입니다. 최대와 최소가 같으면(1.0, 0)입니다.quantize_affine은round_half_even(x / scale) + zero_point를 0 부터UMAX까지로 잘라 냅니다. 자르는 일이 실제로 쓰입니다 — 양 끝이 반올림에서 한 칸씩 밀리면 합이 256 이 됩니다.dequantize_affine은(code - zero_point) * scale입니다.levels_used(codes)는 서로 다른 코드의 개수입니다.error_stats(original, restored)는max_abs·mean_abs·max_rel세 열쇠를 가진 딕셔너리입니다.max_rel은 최대 절대 오차를 원본의 최대 절대값으로 나눈 값입니다 — 값 하나하나로 나누면 0 근처에서 무한대로 튀기 때문입니다. 원본의 최대 절대값이 0 이면max_rel은0.0입니다.outlier_effect(values, outlier)는values만 접었을 때와values + [outlier]를 접었을 때를 각각 재고 원래values자리만 견줍니다. 돌려주는 열쇠는clean_scale·dirty_scale·clean_levels·dirty_levels·clean_max_abs·dirty_max_abs여섯 개입니다.quantize_tensor(matrix)는(배율, 코드 행렬),quantize_rows(matrix)는(배율 목록, 코드 행렬)을 돌려줍니다.granularity_gap(matrix)의 열쇠는tensor_max_abs·row_max_abs·tensor_worst_row_rel·row_worst_row_rel·tensor_worst_row_levels·row_worst_row_levels여섯 개입니다.worst_row_rel은 행마다 그 행의error_stats로max_rel을 구한 뒤 가장 큰 값이고,worst_row_levels는 행마다levels_used를 구한 뒤 가장 작은 값입니다.int_matmul(left_codes, right_codes)는 정수만으로 곱합니다. 중간에 실수가 섞이면 안 됩니다.quant_matmul(left, right)는 왼쪽을 행 단위로, 오른쪽을 열 단위로 접은 뒤int_matmul로 곱하고acc * left_scale * right_scale로 폅니다. 돌려주는 값은(복원 행렬, 정수 누적 행렬, 왼쪽 배율 목록, 오른쪽 배율 목록)네 개짜리 튜플입니다.softmax(scores)는 목록 하나를 받아 확률 목록을 돌려줍니다.attention_shift(queries, keys)는 점수를float_matmul(queries, transpose(keys))로, 근사 점수를quant_matmul(queries, transpose(keys))의 첫 번째 값으로 구한 뒤score_max_abs·prob_max_abs·argmax_changed·top_prob_max_abs네 열쇠를 돌려줍니다. 이 실습은 점수를 그대로 씁니다.- 8단계 재료의 모양:
WEIGHTS는 6행 8열,QUERIES는 4행 8열,KEYS는 5행 8열이고 모든 값의 절대값이 2.0 이하입니다.WEIGHTS는 행마다 폭이 크게 달라야 합니다 — 가장 큰 행의 최대 절대값이 가장 작은 행의 10배 이상이어야 텐서 단위와 행 단위가 갈립니다.OUTLIER는 절대값이 20.0 이상인 값 하나입니다. 숫자는 자유롭게 정하세요. - 8단계 보고서의 이상치 키 행렬은
KEYS를 복사해[0][0]자리만OUTLIER로 바꾼 것입니다. 원본KEYS는 그대로 두세요. - 이 파드에는 인터넷이 없습니다.
pip install은 되지 않고 시스템 파이썬에서import numpy도 되지 않습니다(numpy 는/opt/onnx-lab/bin/python안에만 있습니다).math만으로 충분합니다. - 공식 문서: [정수 산술만으로 추론](https://arxiv.org/abs/1712.05877) · [LLM.int8()](https://arxiv.org/abs/2208.07339) · [Attention Is All You Need](https://arxiv.org/abs/1706.03762) · [Python math](https://docs.python.org/3/library/math.html)
- 흔한 실수:
round_half_away를math.floor(x + 0.5)한 줄로 쓰기, 범위 밖을 안 자르기, 배율을 최대값(절대값이 아닌)으로 잡기,error_stats의 상대 오차를 값마다 따로 나누기,outlier_effect에서 이상치 자신의 오차까지 세기,quant_matmul에서 오른쪽을 열이 아니라 행 단위로 접기,int_matmul안에서 미리 배율을 곱하기.
단계 8개
- 반올림부터 못 박는다
- 대칭 양자화로 접었다 편다
- 비대칭 양자화로 256칸을 다 쓴다
- 얼마나 벌어졌는지 재는 자
- 큰 값 하나가 나머지에게 하는 일
- 텐서 하나의 배율 대 행마다의 배율
- 정수만으로 행렬을 곱한다
- 소프트맥스를 지난 뒤에 남는 것