亲手算一遍 int8 的算术
한국어 원문으로 표시합니다.
목표
정수 양자화의 산술을 표준 라이브러리만으로 직접 한다. 반올림 규칙을 못 박는 것에서 시작해 대칭·비대칭 양자화를 만들고, 접었다 편 값의 오차를 재고, 이상치 하나가 배율을 얼마나 망가뜨리는지 세고, 텐서 단위와 행 단위를 나란히 견주고, 코드끼리 정수로만 곱하고, 마지막으로 그 오차가 소프트맥스를 지나 확률에서 얼마가 되는지 잰다.
왜 중요한가
양자화 도구는 한 줄이다. 그 안에서 무슨 산술이 일어났는지 모르면 정확도가 떨어졌을 때 옵션을 바꿔 가며 다시 돌려 보는 것밖에 할 수 없다. 배율이 무엇으로 정해지는지, 반올림이 어느 쪽으로 가는지, 단위를 좁히면 무엇이 달라지는지를 값 여덟 개짜리 목록에서 한 번 보고 나면 큰 모델에서도 같은 자리를 짚을 수 있다.
이 실습은 도구를 쓰지 않는다. 이 파드의 시스템 파이썬에는 numpy 가 없고(numpy 는 /opt/onnx-lab/bin/python 안에만 있습니다) 모델도 부르지 않는다. 그래서 "어느 모델이 int8 에서 정확도가 몇 퍼센트 떨어진다" 같은 말은 여기서 하지 않는다. 여러분이 만든 목록과 행렬에서 잰 숫자만 쓴다.
어려운 것은 식이 아니라 세부다. 0.5 를 어느 쪽으로 보낼지, 범위 밖을 자를지 말지, 배율을 무엇의 최대값으로 잡을지가 정해져 있지 않으면 같은 입력으로도 코드가 한 칸씩 어긋난다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 입력으로 함수를 두드려 보고, 채점기가 따로 계산한 값과 대조한다. 대조의 대부분은 정수 배열끼리라 흔들림이 없다.
단계
- /root/work/tf-quant/quant.py 에
QMAX = 127·UMAX = 255와round_half_even(x)·round_half_away(x)·rounding_gap(values)를 만드세요. 두 반올림 규칙이 어디서 갈리는지 눈으로 확인합니다. sym_scale(values)·quantize_sym(values, scale)·dequantize_sym(codes, scale)를 더해 대칭 양자화를 만드세요. 배율은max(|x|) / 127입니다.affine_params(values)·quantize_affine(values, scale, zero_point)·dequantize_affine(codes, scale, zero_point)를 더해 비대칭 양자화를 만드세요. 배율은(max - min) / 255입니다.levels_used(codes)와error_stats(original, restored)를 더해 오차를 재는 자를 만드세요.outlier_effect(values, outlier)를 더해 큰 값 하나가 나머지 값들에게 무슨 일을 하는지 재게 하세요.quantize_tensor(matrix)·quantize_rows(matrix)·granularity_gap(matrix)를 더해 텐서 단위와 행 단위를 견주세요.transpose(matrix)·int_matmul(left_codes, right_codes)·float_matmul(left, right)·quant_matmul(left, right)를 더해 정수만으로 행렬을 곱하세요.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만으로 충분합니다. - 공식 문서: 정수 산술만으로 추론 · LLM.int8() · Attention Is All You Need · Python math
- 흔한 실수:
round_half_away를math.floor(x + 0.5)한 줄로 쓰기, 범위 밖을 안 자르기, 배율을 최대값(절대값이 아닌)으로 잡기,error_stats의 상대 오차를 값마다 따로 나누기,outlier_effect에서 이상치 자신의 오차까지 세기,quant_matmul에서 오른쪽을 열이 아니라 행 단위로 접기,int_matmul안에서 미리 배율을 곱하기.
반올림부터 못 박는다
/root/work/tf-quant/quant.py 에 QMAX = 127·UMAX = 255 와 round_half_even(x)·round_half_away(x)·rounding_gap(values) 를 만드세요. 앞의 것은 파이썬 기본 round 그대로 정확히 0.5 를 짝수 쪽으로 보내고, 뒤의 것은 0 에서 먼 쪽으로 보냅니다. rounding_gap 은 두 규칙이 갈리는 값만 받은 순서대로 모아 돌려줍니다.
python3 -c "print(round(0.5), round(1.5), round(2.5))" 를 먼저 돌려 보세요. 0 2 2 가 나옵니다. round_half_away 를 math.floor(x + 0.5) 한 줄로 쓰면 음수에서 틀립니다 — -1.5 는 -2 로 가야 하는데 그 식은 -1 을 줍니다. math.floor(x) 로 아래를 구하고 소수부를 따로 보아 0.5 인 자리만 부호로 갈라 주세요. 두 함수 모두 정수를 돌려줍니다.
대칭 양자화로 접었다 편다
sym_scale(values)·quantize_sym(values, scale)·dequantize_sym(codes, scale) 를 더하세요. 배율은 max(|x|) / QMAX 이고, 값이 전부 0 이면 1.0 입니다. 접을 때는 배율로 나누고 round_half_even 으로 반올림한 뒤 -QMAX 부터 QMAX 까지로 잘라 냅니다.
배율을 최대값이 아니라 최대 절대값으로 잡아야 음수 쪽도 범위 안에 들어옵니다. 자르는 일은 max(-QMAX, min(QMAX, code)) 한 줄이면 됩니다. 펴는 함수는 배율을 곱하는 것뿐이라 한 줄입니다 — 편 값이 원본과 같아지지 않는 것이 정상입니다. 잃은 만큼이 배율의 절반 안에 들어옵니다.
비대칭 양자화로 256칸을 다 쓴다
affine_params(values)·quantize_affine(values, scale, zero_point)·dequantize_affine(codes, scale, zero_point) 를 더하세요. 배율은 (max - min) / UMAX, 영점은 round_half_even(-min / scale) 을 0 부터 UMAX 까지로 자른 값입니다. 최대와 최소가 같으면 (1.0, 0) 입니다. 접을 때는 round_half_even(x / scale) + zero_point 를 0 부터 UMAX 까지로 잘라 냅니다.
자르는 일을 빼먹으면 여기서 실제로 터집니다 — 양 끝이 반올림에서 한 칸씩 밀리면 round(x/scale) + zero_point 가 256 이 되어 uint8 범위를 넘어갑니다. 펴는 식은 (code - zero_point) * scale 입니다. 이 식 덕분에 실수 0 은 오차 없이 되돌아옵니다 — 영점이 있는 이유가 그것입니다.
얼마나 벌어졌는지 재는 자
levels_used(codes) 와 error_stats(original, restored) 를 더하세요. 앞의 것은 서로 다른 코드의 개수이고, 뒤의 것은 max_abs·mean_abs·max_rel 세 열쇠를 가진 딕셔너리입니다. max_rel 은 최대 절대 오차를 원본의 최대 절대값으로 나눈 값이고, 그 최대 절대값이 0 이면 0.0 입니다.
상대 오차를 값 하나하나로 나누면 0 근처의 값에서 무한대로 튑니다. 그래서 목록이 담고 있는 폭으로 나눕니다 — 배율이 그 폭으로 정해지므로 견줄 대상도 그것입니다. levels_used 는 len(set(codes)) 한 줄입니다. 256칸을 두고도 몇 칸만 쓰고 있는지가 다음 단계의 이야기입니다.
큰 값 하나가 나머지에게 하는 일
outlier_effect(values, outlier) 를 더하세요. values 만 접었을 때와 values + [outlier] 를 접었을 때를 각각 재고 원래 values 자리만 견줍니다. 돌려주는 열쇠는 clean_scale·dirty_scale·clean_levels·dirty_levels·clean_max_abs·dirty_max_abs 여섯 개입니다.
이상치를 붙인 목록을 통째로 접은 뒤 앞에서 len(values) 개만 잘라 견주세요. 이상치 자신의 오차까지 세면 이야기가 뒤집힙니다 — 망가지는 것은 이상치가 아니라 그 옆에 있던 평범한 값들입니다. dirty_levels 가 몇으로 떨어지는지 눈으로 보세요. 256칸을 두고 몇 칸을 쓰고 있습니까.
텐서 하나의 배율 대 행마다의 배율
quantize_tensor(matrix)·quantize_rows(matrix)·granularity_gap(matrix) 를 더하세요. 앞의 둘은 각각 (배율, 코드 행렬) 과 (배율 목록, 코드 행렬) 을 돌려줍니다. granularity_gap 의 열쇠는 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 를 구한 뒤 가장 작은 값입니다. 최대 절대 오차만 보면 두 방식의 차이가 거의 안 보입니다 — 그 값은 가장 큰 행이 정하기 때문입니다. 작은 행이 어떻게 뭉개지는지는 상대 오차와 쓴 칸 수에서 드러납니다.
정수만으로 행렬을 곱한다
transpose(matrix)·int_matmul(left_codes, right_codes)·float_matmul(left, right)·quant_matmul(left, right) 를 더하세요. int_matmul 은 곱도 합도 전부 정수여야 합니다. quant_matmul 은 왼쪽을 행 단위로, 오른쪽을 열 단위로 접은 뒤 정수로 곱하고 acc * left_scale * right_scale 로 펴서 (복원 행렬, 정수 누적 행렬, 왼쪽 배율 목록, 오른쪽 배율 목록) 을 돌려줍니다.
오른쪽을 열 단위로 접으려면 transpose 로 열을 꺼내 각 열에 quantize_sym 을 쓰고 다시 transpose 로 되돌리면 됩니다. 누적 안에서는 배율을 절대 곱하지 마세요 — 마지막에 한 번만 곱합니다. 그래서 행렬 곱의 오차는 누적에서 불어난 것이 아니라 처음 접을 때 이미 생긴 것입니다. 채점기는 정수 누적 행렬을 그대로 대조하므로 한 칸만 어긋나도 잡힙니다.
소프트맥스를 지난 뒤에 남는 것
WEIGHTS(6행 8열)·OUTLIER(절대값 20.0 이상)·QUERIES(4행 8열)·KEYS(5행 8열)와 softmax(scores)·attention_shift(queries, keys) 를 만드세요. WEIGHTS 는 가장 큰 행의 최대 절대값이 가장 작은 행의 10배 이상이어야 하고, 세 행렬의 모든 값은 절대값 2.0 이하입니다. 그다음 /root/work/tf-quant/quant_report.json 에 sym_scale·sym_max_abs·sym_mean_abs·sym_max_rel·sym_levels·affine_scale·affine_zero_point·affine_max_abs·affine_levels·outlier_clean_scale·outlier_dirty_scale·outlier_clean_levels·outlier_dirty_levels·outlier_clean_max_abs·outlier_dirty_max_abs·tensor_max_abs·row_max_abs·tensor_worst_row_rel·row_worst_row_rel·tensor_worst_row_levels·row_worst_row_levels·matmul_max_abs·matmul_max_rel·clean_score_max_abs·clean_prob_max_abs·clean_argmax_changed·dirty_score_max_abs·dirty_prob_max_abs·dirty_argmax_changed 를, /root/work/tf-quant/quant_report.md 에 ## 무엇을 쟀나 ## 대칭과 비대칭은 어디서 갈렸나 ## 이상치 하나가 한 일 ## 행렬 곱과 소프트맥스를 지나면 네 절로 쓰세요.
숫자는 손으로 적지 말고 여러분의 코드를 실제로 돌려 얻은 값으로 채우세요. sym_* 과 affine_* 은 WEIGHTS 를 한 줄로 편 목록에 대한 값이고, outlier_* 는 그 목록과 OUTLIER 로 outlier_effect 를 부른 결과입니다. tensor_*·row_* 는 granularity_gap(WEIGHTS) 의 값이고, matmul_* 은 quant_matmul(QUERIES, transpose(KEYS)) 의 복원 행렬을 float_matmul(QUERIES, transpose(KEYS)) 와 견준 error_stats 의 max_abs·max_rel 입니다. clean_* 은 attention_shift(QUERIES, KEYS), dirty_* 는 KEYS 를 복사해 [0][0] 자리만 OUTLIER 로 바꾼 행렬로 부른 결과입니다. 원본 KEYS 는 고치지 마세요. 점수 오차와 확률 오차가 어느 쪽으로 움직이는지 보고 본 대로 적으세요 — 짐작해서 쓰지 마세요.