TT Lab
요금제

한국어

시작하기
블로그

블로그

숫자를 줄이기: FP16·BF16·INT8 양자화, 사후 양자화와 양자화 인식 학습, 보정 데이터

상태: 초안 (2026-10-16), 일부 실측. GPU 측정은 집 랩의 8GB급 노트북용 GPU 한 장에서 1회용 파드로 했습니다(예열 뒤 반복 중앙값). 정확도 비교는 공개 데이터셋 OpenTTGames 시험 경기 하나(표식 있는 60fps 프레임 388장)입니다. INT8 은 같은 파드의 CPU(4코어 한도)에서 PyTorch 의 사후 양자화로 했습니다. 형식과 보정의 원리는 노트북 CPU 예제(code/02_quant_error.py, 1초 안팎)로 확인했습니다. 시리즈 「비전 모델 읽기 3」 2편. 지연 예산은 1편에 있습니다.

목차

1. 왜 비트를 줄이면 빨라지나

신경망 추론의 시간은 대부분 두 가지입니다. 메모리에서 숫자를 가져오는 시간, 그리고 곱하고 더하는 시간. 숫자 하나를 32비트에서 16비트로 줄이면 가져올 바이트가 절반이 됩니다. 게다가 요즘 GPU 와 휴대폰 칩에는 16비트·8비트 곱셈을 한꺼번에 많이 하는 전용 장치가 있습니다. NVIDIA 의 2018년 세대 GPU 사양으로는 INT8 계산 처리량이 FP32 의 16배, FP16 이 8배입니다(Wu 외 2020, 표 1 — 사양 비율이지 실측이 아닙니다).

대가는 정확도입니다. 숫자를 덜 촘촘하게 적으면 반올림 오차가 생기고, 범위가 좁으면 큰 수가 넘칩니다.

숫자 형식과 INT8 눈금 비트 나눔(가로 길이 = 비트 수) FP32 지수 8 가수 23 FP16 지수 5 가수 10 최대 65,504 BF16 지수 8 가수 7 범위 = FP32 INT8 정수 8 −128 … 127 s = 부호. 지수는 범위를, 가수는 촘촘함을 정한다 INT8 양자화: 실수 r 을 0–255 눈금 q 에 맞춘다 q=0 q=255 잘림 최솟값 상한 가장 가까운 눈금으로 반올림 → 255 r = S · (q − Z), S = (상한 − 최솟값) ÷ 255 범위를 넓게 잡으면 잘림은 줄지만 눈금 간격 S 가 커진다 범위를 좁게 잡으면 눈금은 촘촘하지만 큰 값이 잘린다
그림: FP32·FP16·BF16·INT8 의 비트 나눔과, INT8 양자화가 실수를 0–255 눈금에 맞추고 상한을 넘는 값은 자르는 모양. 범위를 넓게 잡으면 잘림은 줄지만 눈금이 성겨진다

2. 16비트 두 가지: FP16 과 BF16

부동소수는 부호·지수·가수로 나뉩니다. 지수는 「얼마나 크거나 작은 수까지」, 가수는 「얼마나 촘촘하게」 를 정합니다.

형식 부호·지수·가수 가장 큰 수 1 다음 수와의 간격(eps)
FP32 1 · 8 · 23 3.4×10³⁸ 1.19×10⁻⁷
FP16 1 · 5 · 10 65,504 9.77×10⁻⁴
BF16 1 · 8 · 7 3.4×10³⁸ 7.81×10⁻³

(code/02_quant_error.out.txt 의 PyTorch 값.) FP16 은 촘촘하지만 범위가 좁고, BF16 은 FP32 와 범위가 같은 대신 성깁니다. 직접 넣어 보면 70,000 은 FP16 에서 inf, BF16 에서 70,144 가 되고, 1e-8 은 FP16 에서 0, BF16 에서 1.00117e-8 이 됩니다. 256×256 행렬곱의 상대 오차는 FP16 3.6×10⁻⁴, BF16 2.9×10⁻³ 이었습니다.

학습에서는 이 범위 문제가 큽니다. Micikevicius 외의 「Mixed Precision Training」(2018)은 FP16 에서 2⁻²⁴ 보다 작은 기울기가 0 이 되는 문제를 손실에 큰 수를 곱해 두었다 나누는 손실 스케일링으로 풀고, 가중치의 FP32 사본을 따로 두었습니다. 그렇게 하면 ResNet-50 의 ImageNet top-1 이 FP32 75.92 와 혼합 정밀도 76.04 로 같았습니다(표 1). BF16 은 범위가 FP32 와 같아 손실 스케일링 없이 쓸 수 있고, Kalamkar 외(2019)는 ResNet-50 을 BF16 으로 학습해 FP32 와 같은 74.7 을 냈습니다(같은 평가 방식끼리 비교, 4.1.2절).

추론에서는 이미 학습된 가중치를 쓰므로 범위 문제가 덜합니다. 활성값이 65,504 를 넘지 않는 한 FP16 으로 충분한 경우가 많습니다.

3. INT8: 실수를 눈금에 맞추기

8비트 정수는 256개의 칸뿐입니다. Jacob 외(2018)의 아핀 양자화는 실수 r 을 이렇게 적습니다.

r = S · (q − Z)
S: 눈금 한 칸의 크기(실수),  Z: 실수 0 에 해당하는 정수(영점),  q: 0–255 의 정수
범위 [최솟값, 상한] 을 정하면  S = (상한 − 최솟값) / 255,  Z = round(−최솟값 / S)

실수 0 이 정확히 정수 하나로 적히도록 Z 를 두는 것이 요점입니다. 0 으로 채우는 패딩과 ReLU 뒤의 0 이 오차 없이 표현됩니다. 곱셈은 uint8 × uint8 을 int32 로 더한 뒤 마지막에 한 번만 배율을 곱해 다시 8비트로 줄이므로, 추론 전체를 정수로만 할 수 있습니다. 같은 논문은 배치 정규화를 합성곱에 미리 접은(3편) 그래프로 양자화합니다.

결과(Jacob 외 2018) 실수 INT8 조건
ResNet-50 ImageNet top-1 76.4 74.9 표 4.1
MobileNet SSD(폭 50%) COCO mAP · 지연 16.7 · 121ms 16.6 · 61ms 2017년 휴대폰 큰 코어, 스레드 하나(표 4.4)
얼굴 검출(폭 25%) 지연 44ms(약 23fps) 28ms(약 36fps) 같은 휴대폰 큰 코어 하나(표 4.6)

같은 33ms 예산 안에서는 INT8 모델이 실수 모델보다 약 10% 더 정확했다는 것이 이 논문의 요지입니다(4.2.1절, 휴대폰의 작은 코어). 정밀도를 낮춰 아낀 시간으로 더 큰 모델을 돌릴 수 있기 때문입니다.

눈금을 몇 개 둘까: 텐서 하나 대 채널마다

가중치 텐서 전체에 S 하나를 쓰면, 한 채널만 값이 커도 눈금이 그 채널에 맞춰 넓어져 나머지가 뭉개집니다. 출력 채널마다 S 를 따로 두면(per-channel) 이 문제가 사라집니다. 직접 해 보니(256×256 행렬곱, 가중치만 INT8)

가중치 텐서 하나 채널마다
고른 가중치 1.06×10⁻² 7.13×10⁻³
한 채널만 20배 7.25×10⁻² (나머지 채널 1.13×10⁻¹) 6.32×10⁻³

입니다. 실제 모델에서도 같은 일이 일어납니다. Krishnamoorthi(2018)의 백서에서 MobileNet-v1 을 학습 없이 8비트로 바꾸면 텐서 하나 눈금은 top-1 0.001(완전히 무너짐), 채널마다 눈금은 0.703(실수 0.709)이었습니다(표 3). 깊이별 합성곱(4편)은 채널마다 값의 크기가 크게 달라 이 문제에 특히 약합니다.

4. 사후 양자화(PTQ)와 보정 데이터

사후 양자화는 학습이 끝난 모델에 대표 입력 몇백 장을 흘려 층마다 활성값의 범위를 재고(보정, calibration), 그 범위로 S·Z 를 정합니다. 학습이 필요 없습니다. Nagel 외(2021)의 백서는 라벨 없는 이미지 500–1,000장이면 대개 충분하다고 적었고, 표준 절차를 따르면 W8A8(가중치·활성값 8비트)에서 다룬 모델 모두 FP32 의 0.7% 안이었다고 보고했습니다.

어려운 것은 범위를 어디까지 잡느냐입니다. 최솟값–최댓값을 그대로 쓰면 드문 큰 값 하나 때문에 눈금이 성겨집니다. 위쪽을 조금 잘라(예: 99.99 백분위) 범위를 좁히면 눈금은 촘촘해지지만 잘린 값이 틀립니다. 두 종류의 활성값으로 직접 해 봤습니다(보정 표본과 시험 표본은 따로 뽑음).

활성값 보정 묶음 최솟값–최댓값 99.99 백분위 99.9 백분위
(가) 꼬리가 긴 분포 1 상한 23.1, 오차 0.057 21.0, 0.067 11.0, 0.158
(가) 64 상한 215.1, 오차 0.144 22.7, 0.059 10.3, 0.170
(나) 드문 큰 값이 신호 64 상한 109.4, 오차 0.076 51.0, 0.180 4.1, 0.707

그래서 보정 방법은 모델마다 재 봐야 합니다. Wu 외(2020)의 표에서 EfficientNet-B0 를 PTQ 하면 최솟값–최댓값 22.3%, 엔트로피 72.1%, 99.9 백분위 70.9% 였고(FP32 76.9%), Inception v4 는 최솟값–최댓값 0.1%, 99.99 백분위 79.6%(FP32 79.7%)였습니다(ImageNet top-1, 보정 1,024장, 표 5).

5. 양자화 인식 학습(QAT)

PTQ 로 모자라면 학습할 때부터 양자화를 흉내 냅니다. 순전파에서 가중치와 활성값을 「양자화했다 되돌린」 값(가짜 양자화)으로 계산하고, 역전파는 반올림이 없는 것처럼 기울기를 그대로 흘립니다. 모델이 눈금에 맞춰 가중치를 옮겨 놓도록 배우는 셈입니다. Krishnamoorthi 백서에서 텐서 하나 눈금으로 무너졌던 MobileNet-v1(0.001)이 QAT 로 0.70 까지 돌아왔고(표 4), Wu 외에서 EfficientNet-B0 는 QAT 로 76.95% 가 되어 PTQ 최선(72.1%)을 넘었습니다(원래 학습 일정의 약 10% 만큼 미세 조정, 표 7). 대가는 학습 파이프라인과 데이터가 필요하다는 것입니다.

6. 우리 실측: 공 모델의 FP16

속도

TT Score 의 공 모델(BlurBall, 3프레임 창, 512×288)을 실험용 GPU 에서 쟀습니다(배치 1, 예열 20회 뒤 250회의 중앙값).

정밀도 창 하나
FP32(운영 방식) 15.09ms
FP16, autocast(연산마다 자동으로 16비트) 9.77ms(1.54배)
FP16, 모델 통째로 half 9.49ms(1.59배)

같은 GPU 에서 트랜스포머 인코더(EfficientTAM-S, 4편)는 3.1배였습니다. 공 모델은 해상도를 끝까지 유지하는 합성곱 망이라 16비트 전용 장치의 몫이 상대적으로 작고, 배치 정규화·ReLU 같은 원소별 연산이 남아 있어서입니다. 3편에서 이것을 컴파일러로 묶으면 4.3ms 까지 내려갔습니다.

정확도: 공 위치가 바뀌나

OpenTTGames 시험 경기 하나에서 공 위치 표식이 있는 60fps 프레임 388장(모두 공 있음)에 대해, 운영과 같은 방식(마지막 프레임의 히트맵, 문턱 0.5 의 덩어리 찾기)으로 FP32·FP16 을 같은 입력에 돌렸습니다. 정답과 5px(512×288 기준) 안이면 맞힌 것으로 셉니다. 같은 프레임을 JPEG 품질 70 으로 압축했다 푼 입력(브라우저가 보내는 것과 같은 조건)도 FP32 로 돌렸습니다.

입력·정밀도 맞힘 / 헛짚음 / 놓침 정밀도 · 재현율 FP32 와 검출 여부가 같은 프레임 히트맵 최댓값 위치가 움직인 프레임
원본 · FP32 384 / 2 / 4 0.995 · 0.990 — —
원본 · FP16(autocast) 384 / 2 / 4 0.995 · 0.990 100% 4.6%(최대 2px)
원본 · FP16(half) 384 / 2 / 4 0.995 · 0.990 100% 0.8%(최대 1px)
JPEG 70 · FP32 377 / 1 / 11 0.997 · 0.972 97.9% 34.3%

FP16 은 이 388장에서 판정을 하나도 바꾸지 않았습니다. 히트맵 값의 차이는 중앙 0.0001 수준이었습니다. 반면 JPEG 압축은 놓친 공을 4개에서 11개로 늘리고, 최댓값 위치를 프레임의 3분의 1 에서 움직였습니다. 정밀도를 반으로 줄이는 것보다 입력을 압축하는 것이 훨씬 큰 오차였습니다. 운영에서는 이미 JPEG 를 받고 있으므로, FP16 을 켜도 지금보다 크게 달라지지 않을 것으로 봅니다. 다만 경기 한 편·조건 하나의 결과이고, 열화 조건(어두움·흐림)과 운영 GPU 에서는 재지 않았습니다(미실측).

7. 우리 실측: 공 모델의 INT8 사후 양자화(CPU)

GPU 의 INT8 은 TensorRT 같은 런타임이 필요해 이번에 재지 못했습니다(3편). 대신 PyTorch 의 사후 양자화(FX 그래프 방식, x86 정수 엔진)로 같은 모델을 CPU 에서 INT8 로 바꿨습니다. 보정 프레임은 경기 앞부분, 평가 프레임은 뒷부분에서 겹치지 않게 골랐습니다.

평가 프레임 220장(공 있음 215), 보정 후보 146장, CPU 4코어 한도, 4장씩 묶어 돌린 창당 시간의 중앙값입니다.

방식 보정 프레임 맞힘 / 헛짚음 / 놓침 재현율 히트맵 최대 차이(FP32 대비, 중앙) CPU 창당
FP32 — 208 / 3 / 7 0.967 — 519.9ms
INT8, 기본(활성값 히스토그램, 가중치 채널마다) 1 206 / 3 / 9 0.958 0.064 72.2ms
같음 8 207 / 3 / 8 0.963 0.056 71.3ms
같음 64 208 / 3 / 7 0.967 0.057 63.1ms
INT8, 최솟값–최댓값, 가중치 텐서 하나 8 207 / 3 / 8 0.963 0.086 71.4ms
INT8, 최솟값–최댓값, 가중치 채널마다 8 208 / 2 / 7 0.967 0.084 72.5ms

8. 한계

9. 우리 프로젝트와의 연결

다음 글은 정밀도는 그대로 두고 실행 방식을 바꾸는 컴파일러와 런타임입니다.

티티 한마디. 숫자를 반으로 줄였는데 공 위치는 그대로였어요. 오히려 사진을 압축할 때 더 흔들렸죠. 어디서 오차가 생기는지 재 보기 전엔 몰라요!

참고 문헌

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다