LabHub
学习 学习路径 课程

Transformer — 手算一遍注意力

亲手算一遍 int8 的算术

在 LabHub 中继续学习

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

목표

정수 양자화의 산술을 표준 라이브러리만으로 직접 한다. 반올림 규칙을 못 박는 것에서 시작해 대칭·비대칭 양자화를 만들고, 접었다 편 값의 오차를 재고, 이상치 하나가 배율을 얼마나 망가뜨리는지 세고, 텐서 단위와 행 단위를 나란히 견주고, 코드끼리 정수로만 곱하고, 마지막으로 그 오차가 소프트맥스를 지나 확률에서 얼마가 되는지 잰다.

왜 중요한가

양자화 도구는 한 줄이다. 그 안에서 무슨 산술이 일어났는지 모르면 정확도가 떨어졌을 때 옵션을 바꿔 가며 다시 돌려 보는 것밖에 할 수 없다. 배율이 무엇으로 정해지는지, 반올림이 어느 쪽으로 가는지, 단위를 좁히면 무엇이 달라지는지를 값 여덟 개짜리 목록에서 한 번 보고 나면 큰 모델에서도 같은 자리를 짚을 수 있다. 이 실습은 도구를 쓰지 않는다. 이 파드의 시스템 파이썬에는 numpy 가 없고(numpy 는 /opt/onnx-lab/bin/python 안에만 있습니다) 모델도 부르지 않는다. 그래서 "어느 모델이 int8 에서 정확도가 몇 퍼센트 떨어진다" 같은 말은 여기서 하지 않는다. 여러분이 만든 목록과 행렬에서 잰 숫자만 쓴다. 어려운 것은 식이 아니라 세부다. 0.5 를 어느 쪽으로 보낼지, 범위 밖을 자를지 말지, 배율을 무엇의 최대값으로 잡을지가 정해져 있지 않으면 같은 입력으로도 코드가 한 칸씩 어긋난다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 입력으로 함수를 두드려 보고, 채점기가 따로 계산한 값과 대조한다. 대조의 대부분은 정수 배열끼리라 흔들림이 없다.

단계

  1. /root/work/tf-quant/quant.pyQMAX = 127·UMAX = 255round_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·KEYSsoftmax(scores)·attention_shift(queries, keys) 를 만들고, /root/work/tf-quant/quant_report.json/root/work/tf-quant/quant_report.md 에 결과를 기록하세요.

참고

반올림부터 못 박는다

/root/work/tf-quant/quant.pyQMAX = 127·UMAX = 255round_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_awaymath.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_usedlen(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_statsmax_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.jsonsym_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_* 는 그 목록과 OUTLIERoutlier_effect 를 부른 결과입니다. tensor_*·row_*granularity_gap(WEIGHTS) 의 값이고, matmul_*quant_matmul(QUERIES, transpose(KEYS)) 의 복원 행렬을 float_matmul(QUERIES, transpose(KEYS)) 와 견준 error_statsmax_abs·max_rel 입니다. clean_*attention_shift(QUERIES, KEYS), dirty_*KEYS 를 복사해 [0][0] 자리만 OUTLIER 로 바꾼 행렬로 부른 결과입니다. 원본 KEYS 는 고치지 마세요. 점수 오차와 확률 오차가 어느 쪽으로 움직이는지 보고 본 대로 적으세요 — 짐작해서 쓰지 마세요.